git.iko.soy/typeable/oauth2-proxy
on master help sign in
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

commit70571d96e163bbf0481d8fc816b8bb3d63747b77 authorDamien Degois <damien@degois.info> dateThu 26 Oct 2023, 11:57 (3 years ago) parent4816e87
9 files changed+108−9
CHANGELOG.md+1 docs/docs/configuration/overview.md+5−1 pkg/apis/options/legacy_options.go+2 pkg/upstream/http.go+34−1 pkg/upstream/http_test.go+23 pkg/upstream/proxy.go+2−2 pkg/upstream/proxy_test.go+26 pkg/upstream/upstream_suite_test.go+14−4 pkg/validation/upstreams.go+1−1
CHANGELOG.md+1
@@ -13,6 +13,7 @@
1313- [#2274](https://github.com/oauth2-proxy/oauth2-proxy/pull/2274) Upgrade golang.org/x/net to v0.17.0 (@pierluigilenoci)
1414- [#2282](https://github.com/oauth2-proxy/oauth2-proxy/pull/2282) Fixed checking Google Groups membership using Google Application Credentials (@kvanzuijlen)
1515- [#2183](https://github.com/oauth2-proxy/oauth2-proxy/pull/2183) Allowing relative redirect url though an option
16+- [#1866](https://github.com/oauth2-proxy/oauth2-proxy/pull/1866) Add support for unix socker as upstream (@babs)
1617-
1718# V7.5.1
1819
docs/docs/configuration/overview.md+5−1
@@ -223,7 +223,11 @@ See below for provider specific options
223223
224224### Upstreams Configuration
225225
226−`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.
226+`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.
227+
228+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.
229+
230+Unix socket upstreams are configured as `unix:///path/to/unix.sock`.
227231
228232Static 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/`.
229233
pkg/apis/options/legacy_options.go+2
@@ -176,6 +176,8 @@ func (l *LegacyUpstreams) convert() (UpstreamConfig, error) {
176176 upstream.ProxyWebSockets = nil
177177 upstream.FlushInterval = nil
178178 upstream.Timeout = nil
179+ case "unix":
180+ upstream.Path = "/"
179181 }
180182
181183 upstreams.Upstreams = append(upstreams.Upstreams, upstream)
pkg/upstream/http.go+34−1
@@ -1,6 +1,8 @@
11package upstream
22
33import (
4+ "context"
5+ "net"
46 "net/http"
57 "net/http/httputil"
68 "net/url"
@@ -18,6 +20,7 @@ const (
1820
1921 httpScheme = "http"
2022 httpsScheme = "https"
23+ unixScheme = "unix"
2124)
2225
2326// SignatureHeaders contains the headers to be signed by the hmac algorithm
@@ -40,7 +43,10 @@ var SignatureHeaders = []string{
4043// to a single upstream host.
4144func newHTTPUpstreamProxy(upstream options.Upstream, u *url.URL, sigData *options.SignatureData, errorHandler ProxyErrorHandler) http.Handler {
4245 // Set path to empty so that request paths start at the server root
43− u.Path = ""
46+ // Unix scheme need the path to find the socket
47+ if u.Scheme != "unix" {
48+ u.Path = ""
49+ }
4450
4551 // Create a ReverseProxy
4652 proxy := newReverseProxy(u, upstream, errorHandler)
@@ -92,6 +98,25 @@ func (h *httpUpstreamProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request)
9298 }
9399}
94100
101+// Unix implementation of http.RoundTripper, required to register unix protocol in reverse proxy
102+type unixRoundTripper struct {
103+ Transport *http.Transport
104+}
105+
106+// Implementation of https://pkg.go.dev/net/http#RoundTripper interface to support http protocol over unix socket
107+func (t *unixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
108+ // Inspired by https://github.com/tv42/httpunix
109+ // Not having a Host, even if not used, makes the reverseproxy fail with a "no Host in request URL"
110+ if req.Host == "" {
111+ req.Host = "localhost"
112+ }
113+ req.URL.Host = req.Host
114+ tt := t.Transport
115+ req = req.Clone(req.Context())
116+ req.URL.Scheme = "http"
117+ return tt.RoundTrip(req)
118+}
119+
95120// newReverseProxy creates a new reverse proxy for proxying requests to upstream
96121// servers based on the upstream configuration provided.
97122// 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
102127 // Inherit default transport options from Go's stdlib
103128 transport := http.DefaultTransport.(*http.Transport).Clone()
104129
130+ if target.Scheme == "unix" {
131+ transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
132+ dialer := net.Dialer{}
133+ return dialer.DialContext(ctx, target.Scheme, target.Path)
134+ }
135+ transport.RegisterProtocol(target.Scheme, &unixRoundTripper{Transport: transport})
136+ }
137+
105138 // Change default duration for waiting for an upstream response
106139 if upstream.Timeout != nil {
107140 transport.ResponseHeaderTimeout = upstream.Timeout.Duration()
pkg/upstream/http_test.go+23
@@ -312,6 +312,29 @@ var _ = Describe("HTTP Upstream Suite", func() {
312312 },
313313 expectedUpstream: "passExistingHostHeader",
314314 }),
315+ Entry("request using UNIX socket upstream", &httpUpstreamTableInput{
316+ id: "unix-upstream",
317+ serverAddr: &unixServerAddr,
318+ target: "http://example.localhost/file",
319+ method: "GET",
320+ body: []byte{},
321+ errorHandler: nil,
322+ expectedResponse: testHTTPResponse{
323+ code: 200,
324+ header: map[string][]string{
325+ contentType: {applicationJSON},
326+ },
327+ request: testHTTPRequest{
328+ Method: "GET",
329+ URL: "http://example.localhost/file",
330+ Header: map[string][]string{},
331+ Body: []byte{},
332+ Host: "example.localhost",
333+ RequestURI: "http://example.localhost/file",
334+ },
335+ },
336+ expectedUpstream: "unix-upstream",
337+ }),
315338 )
316339
317340 It("ServeHTTP, when not passing a host header", func() {
pkg/upstream/proxy.go+2−2
@@ -48,9 +48,9 @@ func NewProxy(upstreams options.UpstreamConfig, sigData *options.SignatureData,
4848 if err := m.registerFileServer(upstream, u, writer); err != nil {
4949 return nil, fmt.Errorf("could not register file upstream %q: %v", upstream.ID, err)
5050 }
51− case httpScheme, httpsScheme:
51+ case httpScheme, httpsScheme, unixScheme:
5252 if err := m.registerHTTPUpstreamProxy(upstream, u, sigData, writer); err != nil {
53− return nil, fmt.Errorf("could not register HTTP upstream %q: %v", upstream.ID, err)
53+ return nil, fmt.Errorf("could not register %s upstream %q: %v", u.Scheme, upstream.ID, err)
5454 }
5555 default:
5656 return nil, fmt.Errorf("unknown scheme for upstream %q: %q", upstream.ID, u.Scheme)
pkg/upstream/proxy_test.go+26
@@ -98,6 +98,11 @@ var _ = Describe("Proxy Suite", func() {
9898 RewriteTarget: "/double-match/rewrite/$1",
9999 URI: serverAddr,
100100 },
101+ {
102+ ID: "unix-upstream",
103+ Path: "/unix/",
104+ URI: unixServerAddr,
105+ },
101106 }
102107 }
103108
@@ -337,6 +342,27 @@ var _ = Describe("Proxy Suite", func() {
337342 },
338343 upstream: "",
339344 }),
345+ Entry("with a request to the UNIX socket backend", &proxyTableInput{
346+ target: "http://example.localhost/unix/file",
347+ response: testHTTPResponse{
348+ code: 200,
349+ header: map[string][]string{
350+ contentType: {applicationJSON},
351+ },
352+ request: testHTTPRequest{
353+ Method: "GET",
354+ URL: "http://example.localhost/unix/file",
355+ Header: map[string][]string{
356+ "Gap-Auth": {""},
357+ "Gap-Signature": {"sha256 4ux8esLj2fw9sTWZwgFhb00bGbw0Fnhed5Fm9jz5Blw="},
358+ },
359+ Body: []byte{},
360+ Host: "example.localhost",
361+ RequestURI: "http://example.localhost/unix/file",
362+ },
363+ },
364+ upstream: "unix-upstream",
365+ }),
340366 )
341367 })
342368
pkg/upstream/upstream_suite_test.go+14−4
@@ -5,6 +5,7 @@ import (
55 "fmt"
66 "io"
77 "log"
8+ "net"
89 "net/http"
910 "net/http/httptest"
1011 "os"
@@ -18,10 +19,12 @@ import (
1819)
1920
2021var (
21− filesDir string
22− server *httptest.Server
23− serverAddr string
24− invalidServer = "http://::1"
22+ filesDir string
23+ server *httptest.Server
24+ serverAddr string
25+ unixServer *httptest.Server
26+ unixServerAddr string
27+ invalidServer = "http://::1"
2528)
2629
2730func TestUpstreamSuite(t *testing.T) {
@@ -46,10 +49,17 @@ var _ = BeforeSuite(func() {
4649 // Set up a webserver that reflects requests
4750 server = httptest.NewServer(&testHTTPUpstream{})
4851 serverAddr = fmt.Sprintf("http://%s", server.Listener.Addr().String())
52+
53+ unixServer = httptest.NewUnstartedServer(&testHTTPUpstream{})
54+ unixListener, _ := net.Listen("unix", path.Join(filesDir, "test.sock"))
55+ unixServer.Listener = unixListener
56+ unixServer.Start()
57+ unixServerAddr = fmt.Sprintf("unix://%s", path.Join(filesDir, "test.sock"))
4958})
5059
5160var _ = AfterSuite(func() {
5261 server.Close()
62+ unixServer.Close()
5363 Expect(os.RemoveAll(filesDir)).To(Succeed())
5464})
5565
pkg/validation/upstreams.go+1−1
@@ -102,7 +102,7 @@ func validateUpstreamURI(upstream options.Upstream) []string {
102102 }
103103
104104 switch u.Scheme {
105− case "http", "https", "file":
105+ case "http", "https", "file", "unix":
106106 // Valid, do nothing
107107 default:
108108 msgs = append(msgs, fmt.Sprintf("upstream %q has invalid scheme: %q", upstream.ID, u.Scheme))