Repository: oauth2-proxy/oauth2-proxy (Go)

Issue:
Feature: support unix socket as upstream
## Expected Behavior

I'd like to be able to provide unix:///run/myapp/uvicorn.sock as the upstream server

~PR is coming~ see PR #1866 

## Your Environment

- Version used: 7.3.0



Discussion:
Seems like a reasonable feature request, all of the upstream proxying code is well maintained and in good form so this shouldn't be too hard. Will want to make sure we have decent tests for this please.
I've submitted a PR, might need some help for the test part
This issue has been inactive for 60 days. If the issue is still relevant please comment to re-activate the issue. If no action is taken within 7 days, the issue will be marked closed.
Still on it.
This issue has been inactive for 60 days. If the issue is still relevant please comment to re-activate the issue. If no action is taken within 7 days, the issue will be marked closed.
Still on it but lack of time :/
This issue has been inactive for 60 days. If the issue is still relevant please comment to re-activate the issue. If no action is taken within 7 days, the issue will be marked closed.
PR still open.
This issue has been inactive for 60 days. If the issue is still relevant please comment to re-activate the issue. If no action is taken within 7 days, the issue will be marked closed.
Not stale
This issue has been inactive for 60 days. If the issue is still relevant please comment to re-activate the issue. If no action is taken within 7 days, the issue will be marked closed.
Still working on this



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf6e7c96b6..31e5b55e14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
 - [#2274](https://github.com/oauth2-proxy/oauth2-proxy/pull/2274) Upgrade golang.org/x/net to v0.17.0 (@pierluigilenoci)
 - [#2282](https://github.com/oauth2-proxy/oauth2-proxy/pull/2282) Fixed checking Google Groups membership using Google Application Credentials (@kvanzuijlen)
 - [#2183](https://github.com/oauth2-proxy/oauth2-proxy/pull/2183) Allowing relative redirect url though an option
+- [#1866](https://github.com/oauth2-proxy/oauth2-proxy/pull/1866) Add support for unix socker as upstream (@babs)
 - 
 # V7.5.1
 
diff --git a/docs/docs/configuration/overview.md b/docs/docs/configuration/overview.md
index 36848ca6f7..e5613c6115 100644
--- a/docs/docs/configuration/overview.md
+++ b/docs/docs/configuration/overview.md
@@ -223,7 +223,11 @@ See below for provider specific options
 
 ### Upstreams Configuration
 
-`oauth2-proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers or serve static files from the file system. HTTP and HTTPS upstreams are configured by providing a URL such as `http://127.0.0.1:8080/` for the upstream parameter. This will forward all authenticated requests to the upstream server. If you instead provide `http://127.0.0.1:8080/some/path/` then it will only be requests that start with `/some/path/` which are forwarded to the upstream.
+`oauth2-proxy` supports having multiple upstreams, and has the option to pass requests on to HTTP(S) servers, unix socket or serve static files from the file system.
+
+HTTP and HTTPS upstreams are configured by providing a URL such as `http://127.0.0.1:8080/` for the upstream parameter. . This will forward all authenticated requests to the upstream server. If you instead provide `http://127.0.0.1:8080/some/path/` then it will only be requests that start with `/some/path/` which are forwarded to the upstream.
+
+Unix socket upstreams are configured as `unix:///path/to/unix.sock`.
 
 Static file paths are configured as a file:// URL. `file:///var/www/static/` will serve the files from that directory at `http://[oauth2-proxy url]/var/www/static/`, which may not be what you want. You can provide the path to where the files should be available by adding a fragment to the configured URL. The value of the fragment will then be used to specify which path the files are available at, e.g. `file:///var/www/static/#/static/` will make `/var/www/static/` available at `http://[oauth2-proxy url]/static/`.
 
diff --git a/pkg/apis/options/legacy_options.go b/pkg/apis/options/legacy_options.go
index 1bb061047e..fcd1f3d3d3 100644
--- a/pkg/apis/options/legacy_options.go
+++ b/pkg/apis/options/legacy_options.go
@@ -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)
diff --git a/pkg/upstream/http.go b/pkg/upstream/http.go
index 031368e543..56fcc23e79 100644
--- a/pkg/upstream/http.go
+++ b/pkg/upstream/http.go
@@ -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()
diff --git a/pkg/upstream/proxy.go b/pkg/upstream/proxy.go
index eabb47ff7e..0f3f574004 100644
--- a/pkg/upstream/proxy.go
+++ b/pkg/upstream/proxy.go
@@ -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)
diff --git a/pkg/validation/upstreams.go b/pkg/validation/upstreams.go
index ba1e216a95..bafed628d7 100644
--- a/pkg/validation/upstreams.go
+++ b/pkg/validation/upstreams.go
@@ -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))


Test patch:
diff --git a/pkg/upstream/http_test.go b/pkg/upstream/http_test.go
index 23753086c3..b18815a2bd 100644
--- a/pkg/upstream/http_test.go
+++ b/pkg/upstream/http_test.go
@@ -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() {
diff --git a/pkg/upstream/proxy_test.go b/pkg/upstream/proxy_test.go
index 87c27e4bfe..374c59c55b 100644
--- a/pkg/upstream/proxy_test.go
+++ b/pkg/upstream/proxy_test.go
@@ -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",
+			}),
 		)
 	})
 
diff --git a/pkg/upstream/upstream_suite_test.go b/pkg/upstream/upstream_suite_test.go
index 95eb681191..ecb8d0b72a 100644
--- a/pkg/upstream/upstream_suite_test.go
+++ b/pkg/upstream/upstream_suite_test.go
@@ -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())
 })