git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add support for systemd.socket

When using sockets to pass data between e.g. nginx and oauth2-proxy it's simpler to use sockets. Systemd can even facilitate this and pass the actual socket directly. This also means that only the socket runs with the same group as nginx while the service runs with DynamicUser. Does not support TLS yet. nginx ``` server { location /oauth2/ { proxy_pass http://unix:/run/oauth2-proxy/oauth2.sock; } ``` oauth2-proxy.socket ``` [Socket] ListenStream=%t/oauth2.sock SocketGroup=www-data SocketMode=0660 ``` Start oauth2-proxy with the parameter `--http-address=fd:3`. Signed-off-by: Josef Johansson <josef@oderland.se>

commit6743a9cc898e49dc92d407be1c571060a09d0632 authorJosef Johansson <josef@oderland.se> dateSun 25 Feb 2024, 12:31 (3 years ago) parentbc8e716
9 files changed+432−3
CHANGELOG.md+2 docs/docs/configuration/overview.md+1−1 docs/docs/configuration/systemd_socket.md+43 docs/docs/installation.md+1 go.mod+1 go.sum+2 pkg/apis/options/legacy_options.go+1−1 pkg/http/server.go+60 pkg/http/server_test.go+321−1
CHANGELOG.md+2
@@ -10,6 +10,7 @@
1010
1111- [#2800](https://github.com/oauth2-proxy/oauth2-proxy/pull/2800) Add some opencontainer labels to docker image (@halkeye)
1212- [#2755](https://github.com/oauth2-proxy/oauth2-proxy/pull/2755) feat: add X-Envoy-External-Address as supported header (@bjencks)
13+- [#1985](https://github.com/oauth2-proxy/oauth2-proxy/pull/1985) Add support for systemd socket (@isodude)
1314
1415# V7.7.1
1516
@@ -58,6 +59,7 @@
5859- [#2790](https://github.com/oauth2-proxy/oauth2-proxy/pull/2790) chore(deps): update all golang dependencies (@tuunit)
5960- [#2607](https://github.com/oauth2-proxy/oauth2-proxy/pull/2607) fix(csrf): fix possible infinite loop (@Primexz)
6061
62+
6163# V7.6.0
6264
6365## Release Highlights
docs/docs/configuration/overview.md+1−1
@@ -219,7 +219,7 @@ Provider specific options can be found on their respective subpages.
219219
220220| Flag / Config Field | Type | Description | Default |
221221| ------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
222−| flag: `--http-address`<br/>toml: `http_address` | string | `[http://]<addr>:<port>` or `unix://<path>` to listen on for HTTP clients. Square brackets are required for ipv6 address, e.g. `http://[::1]:4180` | `"127.0.0.1:4180"` |
222+| flag: `--http-address`<br/>toml: `http_address` | string | `[http://]<addr>:<port>` or `unix://<path>` or `fd:<int>` (case insensitive) to listen on for HTTP clients. Square brackets are required for ipv6 address, e.g. `http://[::1]:4180` | `"127.0.0.1:4180"` |
223223| flag: `--https-address`<br/>toml: `https_address` | string | `[https://]<addr>:<port>` to listen on for HTTPS clients. Square brackets are required for ipv6 address, e.g. `https://[::1]:443` | `":443"` |
224224| flag: `--metrics-address`<br/>toml: `metrics_address` | string | the address prometheus metrics will be scraped from | `""` |
225225| flag: `--metrics-secure-address`<br/>toml: `metrics_secure_address` | string | the address prometheus metrics will be scraped from if using HTTPS | `""` |
docs/docs/configuration/systemd_socket.mdnew file+43
@@ -0,0 +1,43 @@
1+---
2+id: systemd_socket
3+title: Systemd Socket Activation
4+---
5+
6+Pass an existing listener created by systemd.socket to oauth2-proxy.
7+
8+To do this create a socket:
9+
10+oauth2-proxy.socket
11+```
12+[Socket]
13+ListenStream=%t/oauth2.sock
14+SocketGroup=www-data
15+SocketMode=0660
16+```
17+
18+Now it's possible to call this socket from e.g. nginx:
19+```
20+server {
21+ location /oauth2/ {
22+ proxy_pass http://unix:/run/oauth2-proxy/oauth2.sock;
23+}
24+```
25+
26+The oauth2-proxy should have `--http-address=fd:3` as a parameter.
27+Here fd is case insensitive and means file descriptor. The number 3 refers to the first non-stdin/stdout/stderr file descriptor,
28+systemd-socket-activate (which is what systemd.socket uses), listens to what it is told and passes
29+the listener it created onto the process, starting with file descriptor 3.
30+
31+```
32+./oauth2-proxy \
33+ --http-address="fd:3" \
34+ --email-domain="yourcompany.com" \
35+ --upstream=http://127.0.0.1:8080/ \
36+ --cookie-secret=... \
37+ --cookie-secure=true \
38+ --provider=... \
39+ --client-id=... \
40+ --client-secret=...
41+```
42+
43+Currently TLS is not supported (but it's doable).
docs/docs/installation.md+1
@@ -29,3 +29,4 @@ title: Installation
29292. [Select a Provider and Register an OAuth Application with a Provider](configuration/providers/index.md)
30303. [Configure OAuth2 Proxy using config file, command line options, or environment variables](configuration/overview.md)
31314. [Configure SSL or Deploy behind an SSL endpoint](configuration/tls.md) (example provided for Nginx)
32+5. [Configure OAuth2 Proxy using systemd.socket](configuration/systemd_socket.md) (example provided for Nginx/Systemd)
go.mod+1
@@ -11,6 +11,7 @@ require (
1111 github.com/bitly/go-simplejson v0.5.1
1212 github.com/bsm/redislock v0.9.4
1313 github.com/coreos/go-oidc/v3 v3.11.0
14+ github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
1415 github.com/fsnotify/fsnotify v1.7.0
1516 github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
1617 github.com/go-jose/go-jose/v3 v3.0.3
go.sum+2
@@ -42,6 +42,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
4242github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
4343github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
4444github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
45+github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
46+github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
4547github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4648github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4749github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
pkg/apis/options/legacy_options.go+1−1
@@ -470,7 +470,7 @@ func legacyServerFlagset() *pflag.FlagSet {
470470 flagSet.String("metrics-secure-address", "", "the address /metrics will be served on for HTTPS clients (e.g. \":9100\")")
471471 flagSet.String("metrics-tls-cert-file", "", "path to certificate file for secure metrics server")
472472 flagSet.String("metrics-tls-key-file", "", "path to private key file for secure metrics server")
473− flagSet.String("http-address", "127.0.0.1:4180", "[http://]<addr>:<port> or unix://<path> to listen on for HTTP clients")
473+ flagSet.String("http-address", "127.0.0.1:4180", "[http://]<addr>:<port> or unix://<path> or fd:<int> (case insensitive) to listen on for HTTP clients")
474474 flagSet.String("https-address", ":443", "<addr>:<port> to listen on for HTTPS clients")
475475 flagSet.String("tls-cert-file", "", "path to certificate file")
476476 flagSet.String("tls-key-file", "", "path to private key file")
pkg/http/server.go+60
@@ -7,15 +7,27 @@ import (
77 "fmt"
88 "net"
99 "net/http"
10+ "os"
11+ "strconv"
1012 "strings"
1113 "time"
1214
15+ "github.com/coreos/go-systemd/activation"
1316 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
1417 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options/util"
1518 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1619 "golang.org/x/sync/errgroup"
1720)
1821
22+// listenFdsStart corresponds to `SD_LISTEN_FDS_START`.
23+// Since the 3 first file descriptors in every linux process is
24+// stdin, stdout and stderr. The first usable file descriptor is 3.
25+// systemd-socket-activate will always assume that the first socket will be
26+// 3 and the rest follow.
27+const (
28+ listenFdsStart = 3
29+)
30+
1931// Server represents an HTTP or HTTPS server.
2032type Server interface {
2133 // Start blocks and runs the server.
@@ -35,6 +47,9 @@ type Opts struct {
3547
3648 // TLS is the TLS configuration for the server.
3749 TLS *options.TLS
50+
51+ // Let testing infrastructure circumvent parsing file descriptors
52+ fdFiles []*os.File
3853}
3954
4055// NewServer creates a new Server from the options given.
@@ -42,6 +57,11 @@ func NewServer(opts Opts) (Server, error) {
4257 s := &server{
4358 handler: opts.Handler,
4459 }
60+
61+ if len(opts.fdFiles) > 0 {
62+ s.fdFiles = opts.fdFiles
63+ }
64+
4565 if err := s.setupListener(opts); err != nil {
4666 return nil, fmt.Errorf("error setting up listener: %v", err)
4767 }
@@ -58,6 +78,30 @@ type server struct {
5878
5979 listener net.Listener
6080 tlsListener net.Listener
81+
82+ // ensure activation.Files are called once
83+ fdFiles []*os.File
84+}
85+
86+// convert a string filedescriptor to an actual listener
87+func (s *server) fdToListener(bindAddress string) (net.Listener, error) {
88+ fd, err := strconv.Atoi(bindAddress)
89+ if err != nil {
90+ return nil, fmt.Errorf("listen failed: fd with name is not implemented yet")
91+ }
92+ fdIndex := fd - listenFdsStart
93+
94+ if len(s.fdFiles) == 0 {
95+ s.fdFiles = activation.Files(true)
96+ }
97+
98+ l := len(s.fdFiles)
99+
100+ if fdIndex < 0 || fdIndex >= l || l == 0 {
101+ return nil, fmt.Errorf("listen failed: fd outside of range of available file descriptors")
102+ }
103+
104+ return net.FileListener(s.fdFiles[fdIndex])
61105}
62106
63107// setupListener sets the server listener if the HTTP server is enabled.
@@ -69,6 +113,22 @@ func (s *server) setupListener(opts Opts) error {
69113 return nil
70114 }
71115
116+ // Use fd: as a prefix for systemd socket activation, it's generic
117+ // enough and short.
118+ // The most common usage would be --http-address fd:3.
119+ // This causes oauth2-proxy to just assume that the third fd passed
120+ // to the program is indeed a net.Listener and starts using it
121+ // without setting up a new listener.
122+ if strings.HasPrefix(strings.ToLower(opts.BindAddress), "fd:") {
123+ listenAddr := opts.BindAddress[3:]
124+ listener, err := s.fdToListener(listenAddr)
125+ if err != nil {
126+ err = fmt.Errorf("listen (%s, %s) failed: %v", "file", listenAddr, err)
127+ }
128+ s.listener = listener
129+ return err
130+ }
131+
72132 networkType := getNetworkScheme(opts.BindAddress)
73133 listenAddr := getListenAddress(opts.BindAddress)
74134
pkg/http/server_test.go+321−1
@@ -5,6 +5,7 @@ import (
55 "errors"
66 "fmt"
77 "io"
8+ "net"
89 "net/http"
910 "os"
1011
@@ -17,16 +18,17 @@ import (
1718const hello = "Hello World!"
1819
1920var _ = Describe("Server", func() {
21+
2022 handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
2123 rw.Write([]byte(hello))
2224 })
23−
2425 Context("NewServer", func() {
2526 type newServerTableInput struct {
2627 opts Opts
2728 expectedErr error
2829 expectHTTPListener bool
2930 expectTLSListener bool
31+ fdAddr string
3032 ipv6 bool
3133 }
3234
@@ -34,6 +36,15 @@ var _ = Describe("Server", func() {
3436 if in.ipv6 {
3537 skipDevContainer()
3638 }
39+
40+ if in.fdAddr != "" {
41+ l, err := net.Listen("tcp", in.fdAddr)
42+ Expect(err).ToNot(HaveOccurred())
43+ f, err := l.(*net.TCPListener).File()
44+ Expect(err).ToNot(HaveOccurred())
45+ in.opts.fdFiles = []*os.File{f}
46+ }
47+
3748 srv, err := NewServer(in.opts)
3849 if in.expectedErr != nil {
3950 Expect(err).To(MatchError(ContainSubstring(in.expectedErr.Error())))
@@ -55,6 +66,46 @@ var _ = Describe("Server", func() {
5566 Expect(s.tlsListener.Close()).To(Succeed())
5667 }
5768 },
69+ Entry("with a valid non-lowercase fd IPv4 bind address", &newServerTableInput{
70+ opts: Opts{
71+ Handler: handler,
72+ BindAddress: "Fd:3",
73+ },
74+ expectedErr: nil,
75+ expectHTTPListener: true,
76+ expectTLSListener: false,
77+ fdAddr: "127.0.0.1:0",
78+ }),
79+ Entry("with a valid fd IPv4 bind address", &newServerTableInput{
80+ opts: Opts{
81+ Handler: handler,
82+ BindAddress: "fd:3",
83+ },
84+ expectedErr: nil,
85+ expectHTTPListener: true,
86+ expectTLSListener: false,
87+ fdAddr: "127.0.0.1:0",
88+ }),
89+ Entry("with a invalid fd named bind address", &newServerTableInput{
90+ opts: Opts{
91+ Handler: handler,
92+ BindAddress: "fd:hello",
93+ },
94+ expectedErr: fmt.Errorf("error setting up listener: listen (file, %s) failed: listen failed: fd with name is not implemented yet", "hello"),
95+ expectHTTPListener: true,
96+ expectTLSListener: false,
97+ fdAddr: "127.0.0.1:0",
98+ }),
99+ Entry("with a invalid fd IPv4 bind address", &newServerTableInput{
100+ opts: Opts{
101+ Handler: handler,
102+ BindAddress: "fd:4",
103+ },
104+ expectedErr: fmt.Errorf("error setting up listener: listen (file, %d) failed: listen failed: fd outside of range of available file descriptors", 4),
105+ expectHTTPListener: true,
106+ expectTLSListener: false,
107+ fdAddr: "127.0.0.1:0",
108+ }),
58109 Entry("with an ipv4 valid http bind address", &newServerTableInput{
59110 opts: Opts{
60111 Handler: handler,
@@ -86,6 +137,21 @@ var _ = Describe("Server", func() {
86137 expectHTTPListener: false,
87138 expectTLSListener: true,
88139 }),
140+ Entry("with a both a fd valid http and ipv4 valid https bind address, and valid TLS config", &newServerTableInput{
141+ opts: Opts{
142+ Handler: handler,
143+ BindAddress: "fd:3",
144+ SecureBindAddress: "127.0.0.1:0",
145+ TLS: &options.TLS{
146+ Key: &ipv4KeyDataSource,
147+ Cert: &ipv4CertDataSource,
148+ },
149+ },
150+ expectedErr: nil,
151+ expectHTTPListener: true,
152+ expectTLSListener: true,
153+ fdAddr: "127.0.0.1:0",
154+ }),
89155 Entry("with a both a ipv4 valid http and ipv4 valid https bind address, and valid TLS config", &newServerTableInput{
90156 opts: Opts{
91157 Handler: handler,
@@ -300,6 +366,27 @@ var _ = Describe("Server", func() {
300366 expectHTTPListener: false,
301367 expectTLSListener: true,
302368 }),
369+ Entry("with valid fd IPv6 bind address", &newServerTableInput{
370+ opts: Opts{
371+ Handler: handler,
372+ BindAddress: "fd:3",
373+ },
374+ expectedErr: nil,
375+ expectHTTPListener: true,
376+ expectTLSListener: false,
377+ fdAddr: "[::1]:0",
378+ ipv6: true,
379+ }),
380+ Entry("with a invalid fd IPv6 bind address", &newServerTableInput{
381+ opts: Opts{
382+ Handler: handler,
383+ BindAddress: "fd:4",
384+ },
385+ expectedErr: fmt.Errorf("error setting up listener: listen (file, %d) failed: listen failed: fd outside of range of available file descriptors", 4),
386+ expectHTTPListener: true,
387+ expectTLSListener: false,
388+ fdAddr: "[::1]:0",
389+ }),
303390 Entry("with an ipv6 valid http bind address", &newServerTableInput{
304391 opts: Opts{
305392 Handler: handler,
@@ -334,6 +421,22 @@ var _ = Describe("Server", func() {
334421 expectTLSListener: true,
335422 ipv6: true,
336423 }),
424+ Entry("with a both a fd valid http and ipv6 valid https bind address, and valid TLS config", &newServerTableInput{
425+ opts: Opts{
426+ Handler: handler,
427+ BindAddress: "fd:3",
428+ SecureBindAddress: "[::1]:0",
429+ TLS: &options.TLS{
430+ Key: &ipv6KeyDataSource,
431+ Cert: &ipv6CertDataSource,
432+ },
433+ },
434+ expectedErr: nil,
435+ expectHTTPListener: true,
436+ expectTLSListener: true,
437+ fdAddr: "[::1]:0",
438+ ipv6: true,
439+ }),
337440 Entry("with a both a ipv6 valid http and ipv6 valid https bind address, and valid TLS config", &newServerTableInput{
338441 opts: Opts{
339442 Handler: handler,
@@ -563,6 +666,58 @@ var _ = Describe("Server", func() {
563666
564667 })
565668
669+ Context("with an fd ipv4 http server", func() {
670+ var listenAddr string
671+
672+ BeforeEach(func() {
673+ l, err := net.Listen("tcp", "127.0.0.1:0")
674+ Expect(err).ToNot(HaveOccurred())
675+ f, err := l.(*net.TCPListener).File()
676+ Expect(err).ToNot(HaveOccurred())
677+
678+ srv, err = NewServer(Opts{
679+ Handler: handler,
680+ BindAddress: "fd:3",
681+ fdFiles: []*os.File{f},
682+ })
683+ Expect(err).ToNot(HaveOccurred())
684+
685+ listenAddr = fmt.Sprintf("http://%s/", l.Addr().String())
686+ })
687+
688+ It("Starts the server and serves the handler", func() {
689+ go func() {
690+ defer GinkgoRecover()
691+ Expect(srv.Start(ctx)).To(Succeed())
692+ }()
693+
694+ resp, err := httpGet(ctx, listenAddr)
695+ Expect(err).ToNot(HaveOccurred())
696+ Expect(resp.StatusCode).To(Equal(http.StatusOK))
697+
698+ body, err := io.ReadAll(resp.Body)
699+ Expect(err).ToNot(HaveOccurred())
700+ Expect(string(body)).To(Equal(hello))
701+ })
702+
703+ It("Stops the server when the context is cancelled", func() {
704+ go func() {
705+ defer GinkgoRecover()
706+ Expect(srv.Start(ctx)).To(Succeed())
707+ }()
708+
709+ _, err := httpGet(ctx, listenAddr)
710+ Expect(err).ToNot(HaveOccurred())
711+
712+ cancel()
713+
714+ Eventually(func() error {
715+ _, err := httpGet(ctx, listenAddr)
716+ return err
717+ }).Should(HaveOccurred())
718+ })
719+ })
720+
566721 Context("with an ipv4 http server", func() {
567722 var listenAddr string
568723
@@ -682,6 +837,88 @@ var _ = Describe("Server", func() {
682837 })
683838 })
684839
840+ Context("with a fd ipv4 http and an ipv4 https server", func() {
841+ var listenAddr, secureListenAddr string
842+
843+ BeforeEach(func() {
844+ l, err := net.Listen("tcp", "127.0.0.1:0")
845+ Expect(err).ToNot(HaveOccurred())
846+ f, err := l.(*net.TCPListener).File()
847+ Expect(err).ToNot(HaveOccurred())
848+
849+ srv, err = NewServer(Opts{
850+ Handler: handler,
851+ BindAddress: "fd:3",
852+ fdFiles: []*os.File{f},
853+ SecureBindAddress: "127.0.0.1:0",
854+ TLS: &options.TLS{
855+ Key: &ipv4KeyDataSource,
856+ Cert: &ipv4CertDataSource,
857+ },
858+ })
859+ Expect(err).ToNot(HaveOccurred())
860+
861+ s, ok := srv.(*server)
862+ Expect(ok).To(BeTrue())
863+
864+ listenAddr = fmt.Sprintf("http://%s/", l.Addr().String())
865+ secureListenAddr = fmt.Sprintf("https://%s/", s.tlsListener.Addr().String())
866+ })
867+
868+ It("Starts the server and serves the handler on http", func() {
869+ go func() {
870+ defer GinkgoRecover()
871+ Expect(srv.Start(ctx)).To(Succeed())
872+ }()
873+
874+ resp, err := httpGet(ctx, listenAddr)
875+ Expect(err).ToNot(HaveOccurred())
876+ Expect(resp.StatusCode).To(Equal(http.StatusOK))
877+
878+ body, err := io.ReadAll(resp.Body)
879+ Expect(err).ToNot(HaveOccurred())
880+ Expect(string(body)).To(Equal(hello))
881+ })
882+
883+ It("Starts the server and serves the handler on https", func() {
884+ go func() {
885+ defer GinkgoRecover()
886+ Expect(srv.Start(ctx)).To(Succeed())
887+ }()
888+
889+ resp, err := httpGet(ctx, secureListenAddr)
890+ Expect(err).ToNot(HaveOccurred())
891+ Expect(resp.StatusCode).To(Equal(http.StatusOK))
892+
893+ body, err := io.ReadAll(resp.Body)
894+ Expect(err).ToNot(HaveOccurred())
895+ Expect(string(body)).To(Equal(hello))
896+ })
897+
898+ It("Stops both servers when the context is cancelled", func() {
899+ go func() {
900+ defer GinkgoRecover()
901+ Expect(srv.Start(ctx)).To(Succeed())
902+ }()
903+
904+ _, err := httpGet(ctx, listenAddr)
905+ Expect(err).ToNot(HaveOccurred())
906+ _, err = httpGet(ctx, secureListenAddr)
907+ Expect(err).ToNot(HaveOccurred())
908+
909+ cancel()
910+
911+ Eventually(func() error {
912+ _, err := httpGet(ctx, listenAddr)
913+ return err
914+ }).Should(HaveOccurred())
915+ Eventually(func() error {
916+ _, err := httpGet(ctx, secureListenAddr)
917+ return err
918+ }).Should(HaveOccurred())
919+ })
920+ })
921+
685922 Context("with both an ipv4 http and an ipv4 https server", func() {
686923 var listenAddr, secureListenAddr string
687924
@@ -880,6 +1117,89 @@ var _ = Describe("Server", func() {
8801117 })
8811118 })
8821119
1120+ Context("with an fd ipv6 http and an ipv6 https server", func() {
1121+ var listenAddr, secureListenAddr string
1122+
1123+ BeforeEach(func() {
1124+ skipDevContainer()
1125+ l, err := net.Listen("tcp", "[::1]:0")
1126+ Expect(err).ToNot(HaveOccurred())
1127+ f, err := l.(*net.TCPListener).File()
1128+ Expect(err).ToNot(HaveOccurred())
1129+
1130+ srv, err = NewServer(Opts{
1131+ Handler: handler,
1132+ BindAddress: "fd:3",
1133+ fdFiles: []*os.File{f},
1134+ SecureBindAddress: "[::1]:0",
1135+ TLS: &options.TLS{
1136+ Key: &ipv6KeyDataSource,
1137+ Cert: &ipv6CertDataSource,
1138+ },
1139+ })
1140+ Expect(err).ToNot(HaveOccurred())
1141+
1142+ s, ok := srv.(*server)
1143+ Expect(ok).To(BeTrue())
1144+
1145+ listenAddr = fmt.Sprintf("http://%s/", l.Addr().String())
1146+ secureListenAddr = fmt.Sprintf("https://%s/", s.tlsListener.Addr().String())
1147+ })
1148+
1149+ It("Starts the server and serves the handler on http", func() {
1150+ go func() {
1151+ defer GinkgoRecover()
1152+ Expect(srv.Start(ctx)).To(Succeed())
1153+ }()
1154+
1155+ resp, err := httpGet(ctx, listenAddr)
1156+ Expect(err).ToNot(HaveOccurred())
1157+ Expect(resp.StatusCode).To(Equal(http.StatusOK))
1158+
1159+ body, err := io.ReadAll(resp.Body)
1160+ Expect(err).ToNot(HaveOccurred())
1161+ Expect(string(body)).To(Equal(hello))
1162+ })
1163+
1164+ It("Starts the server and serves the handler on https", func() {
1165+ go func() {
1166+ defer GinkgoRecover()
1167+ Expect(srv.Start(ctx)).To(Succeed())
1168+ }()
1169+
1170+ resp, err := httpGet(ctx, secureListenAddr)
1171+ Expect(err).ToNot(HaveOccurred())
1172+ Expect(resp.StatusCode).To(Equal(http.StatusOK))
1173+
1174+ body, err := io.ReadAll(resp.Body)
1175+ Expect(err).ToNot(HaveOccurred())
1176+ Expect(string(body)).To(Equal(hello))
1177+ })
1178+
1179+ It("Stops both servers when the context is cancelled", func() {
1180+ go func() {
1181+ defer GinkgoRecover()
1182+ Expect(srv.Start(ctx)).To(Succeed())
1183+ }()
1184+
1185+ _, err := httpGet(ctx, listenAddr)
1186+ Expect(err).ToNot(HaveOccurred())
1187+ _, err = httpGet(ctx, secureListenAddr)
1188+ Expect(err).ToNot(HaveOccurred())
1189+
1190+ cancel()
1191+
1192+ Eventually(func() error {
1193+ _, err := httpGet(ctx, listenAddr)
1194+ return err
1195+ }).Should(HaveOccurred())
1196+ Eventually(func() error {
1197+ _, err := httpGet(ctx, secureListenAddr)
1198+ return err
1199+ }).Should(HaveOccurred())
1200+ })
1201+ })
1202+
8831203 Context("with both an ipv6 http and an ipv6 https server", func() {
8841204 var listenAddr, secureListenAddr string
8851205