Add support for unix socket as upstream (#1866)

* Add support for unix socket as upstream

* Add CHANGELOG.md entry

* Add Unix socket documentation

* Don't export unixRoundTripper, switch from string prefix to Scheme match

* Add basic unix server mock

* Add some tests and comments
This commit is contained in:
Damien Degois
2023-10-26 10:57:00 +01:00
committed by GitHub
parent 4816e87316
commit 70571d96e1
9 changed files with 108 additions and 9 deletions
+2
View File
@@ -176,6 +176,8 @@ func (l *LegacyUpstreams) convert() (UpstreamConfig, error) {
upstream.ProxyWebSockets = nil
upstream.FlushInterval = nil
upstream.Timeout = nil
case "unix":
upstream.Path = "/"
}
upstreams.Upstreams = append(upstreams.Upstreams, upstream)
+34 -1
View File
@@ -1,6 +1,8 @@
package upstream
import (
"context"
"net"
"net/http"
"net/http/httputil"
"net/url"
@@ -18,6 +20,7 @@ const (
httpScheme = "http"
httpsScheme = "https"
unixScheme = "unix"
)
// SignatureHeaders contains the headers to be signed by the hmac algorithm
@@ -40,7 +43,10 @@ var SignatureHeaders = []string{
// to a single upstream host.
func newHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *options.SignatureData, errorHandler ProxyErrorHandler) http.Handler {
// Set path to empty so that request paths start at the server root
u.Path = ""
// Unix scheme need the path to find the socket
if u.Scheme != "unix" {
u.Path = ""
}
// Create a ReverseProxy
proxy := newReverseProxy(u, upstream, errorHandler)
@@ -92,6 +98,25 @@ func (h *httpUpstreamProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request)
}
}
// Unix implementation of http.RoundTripper, required to register unix protocol in reverse proxy
type unixRoundTripper struct {
Transport *http.Transport
}
// Implementation of https://pkg.go.dev/net/http#RoundTripper interface to support http protocol over unix socket
func (t *unixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// Inspired by https://github.com/tv42/httpunix
// Not having a Host, even if not used, makes the reverseproxy fail with a "no Host in request URL"
if req.Host == "" {
req.Host = "localhost"
}
req.URL.Host = req.Host
tt := t.Transport
req = req.Clone(req.Context())
req.URL.Scheme = "http"
return tt.RoundTrip(req)
}
// newReverseProxy creates a new reverse proxy for proxying requests to upstream
// servers based on the upstream configuration provided.
// The proxy should render an error page if there are failures connecting to the
@@ -102,6 +127,14 @@ func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler Pr
// Inherit default transport options from Go's stdlib
transport := http.DefaultTransport.(*http.Transport).Clone()
if target.Scheme == "unix" {
transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, target.Scheme, target.Path)
}
transport.RegisterProtocol(target.Scheme, &unixRoundTripper{Transport: transport})
}
// Change default duration for waiting for an upstream response
if upstream.Timeout != nil {
transport.ResponseHeaderTimeout = upstream.Timeout.Duration()
+23
View File
@@ -312,6 +312,29 @@ var _ = Describe("HTTP Upstream Suite", func() {
},
expectedUpstream: "passExistingHostHeader",
}),
Entry("request using UNIX socket upstream", &httpUpstreamTableInput{
id: "unix-upstream",
serverAddr: &unixServerAddr,
target: "http://example.localhost/file",
method: "GET",
body: []byte{},
errorHandler: nil,
expectedResponse: testHTTPResponse{
code: 200,
header: map[string][]string{
contentType: {applicationJSON},
},
request: testHTTPRequest{
Method: "GET",
URL: "http://example.localhost/file",
Header: map[string][]string{},
Body: []byte{},
Host: "example.localhost",
RequestURI: "http://example.localhost/file",
},
},
expectedUpstream: "unix-upstream",
}),
)
It("ServeHTTP, when not passing a host header", func() {
+2 -2
View File
@@ -48,9 +48,9 @@ func NewProxy(upstreams options.UpstreamConfig, sigData *options.SignatureData,
if err := m.registerFileServer(upstream, u, writer); err != nil {
return nil, fmt.Errorf("could not register file upstream %q: %v", upstream.ID, err)
}
case httpScheme, httpsScheme:
case httpScheme, httpsScheme, unixScheme:
if err := m.registerHTTPUpstreamProxy(upstream, u, sigData, writer); err != nil {
return nil, fmt.Errorf("could not register HTTP upstream %q: %v", upstream.ID, err)
return nil, fmt.Errorf("could not register %s upstream %q: %v", u.Scheme, upstream.ID, err)
}
default:
return nil, fmt.Errorf("unknown scheme for upstream %q: %q", upstream.ID, u.Scheme)
+26
View File
@@ -98,6 +98,11 @@ var _ = Describe("Proxy Suite", func() {
RewriteTarget: "/double-match/rewrite/$1",
URI: serverAddr,
},
{
ID: "unix-upstream",
Path: "/unix/",
URI: unixServerAddr,
},
}
}
@@ -337,6 +342,27 @@ var _ = Describe("Proxy Suite", func() {
},
upstream: "",
}),
Entry("with a request to the UNIX socket backend", &proxyTableInput{
target: "http://example.localhost/unix/file",
response: testHTTPResponse{
code: 200,
header: map[string][]string{
contentType: {applicationJSON},
},
request: testHTTPRequest{
Method: "GET",
URL: "http://example.localhost/unix/file",
Header: map[string][]string{
"Gap-Auth": {""},
"Gap-Signature": {"sha256 4ux8esLj2fw9sTWZwgFhb00bGbw0Fnhed5Fm9jz5Blw="},
},
Body: []byte{},
Host: "example.localhost",
RequestURI: "http://example.localhost/unix/file",
},
},
upstream: "unix-upstream",
}),
)
})
+14 -4
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httptest"
"os"
@@ -18,10 +19,12 @@ import (
)
var (
filesDir string
server *httptest.Server
serverAddr string
invalidServer = "http://::1"
filesDir string
server *httptest.Server
serverAddr string
unixServer *httptest.Server
unixServerAddr string
invalidServer = "http://::1"
)
func TestUpstreamSuite(t *testing.T) {
@@ -46,10 +49,17 @@ var _ = BeforeSuite(func() {
// Set up a webserver that reflects requests
server = httptest.NewServer(&testHTTPUpstream{})
serverAddr = fmt.Sprintf("http://%s", server.Listener.Addr().String())
unixServer = httptest.NewUnstartedServer(&testHTTPUpstream{})
unixListener, _ := net.Listen("unix", path.Join(filesDir, "test.sock"))
unixServer.Listener = unixListener
unixServer.Start()
unixServerAddr = fmt.Sprintf("unix://%s", path.Join(filesDir, "test.sock"))
})
var _ = AfterSuite(func() {
server.Close()
unixServer.Close()
Expect(os.RemoveAll(filesDir)).To(Succeed())
})
+1 -1
View File
@@ -102,7 +102,7 @@ func validateUpstreamURI(upstream options.Upstream) []string {
}
switch u.Scheme {
case "http", "https", "file":
case "http", "https", "file", "unix":
// Valid, do nothing
default:
msgs = append(msgs, fmt.Sprintf("upstream %q has invalid scheme: %q", upstream.ID, u.Scheme))