Skip to content

Commit 9b6be22

Browse files
Support listening on file descriptors (#380)
Fixes caddyserver/caddy#7525.
1 parent c7496f1 commit 9b6be22

4 files changed

Lines changed: 128 additions & 9 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build !unix
2+
3+
package filedescriptor
4+
5+
import "fmt"
6+
7+
var IsUnix = false
8+
9+
func Dup(fd int) (int, error) {
10+
return 0, fmt.Errorf("File descriptor duplication is not supported on this platform")
11+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build unix
2+
3+
package filedescriptor
4+
5+
import (
6+
"golang.org/x/sys/unix"
7+
)
8+
9+
var IsUnix = true
10+
11+
func Dup(fd int) (int, error) {
12+
return unix.Dup(fd)
13+
}

solvers.go

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@ import (
2222
"log"
2323
"net"
2424
"net/http"
25+
"os"
2526
"path"
2627
"runtime"
28+
"strconv"
2729
"strings"
2830
"sync"
2931
"sync/atomic"
3032
"time"
3133

34+
"github.com/caddyserver/certmagic/internal/filedescriptor"
3235
"github.com/libdns/libdns"
3336
"github.com/mholt/acmez/v3"
3437
"github.com/mholt/acmez/v3/acme"
@@ -708,6 +711,7 @@ func getSolverInfo(address string) *solverInfo {
708711
// which is useful for our challenge servers, where we assume
709712
// that whatever is already listening can solve the challenges.
710713
func robustTryListen(addr string) (net.Listener, error) {
714+
711715
var listenErr error
712716
for i := 0; i < 2; i++ {
713717
// doesn't hurt to sleep briefly before the second
@@ -718,17 +722,56 @@ func robustTryListen(addr string) (net.Listener, error) {
718722

719723
// if we can bind the socket right away, great!
720724
var ln net.Listener
721-
ln, listenErr = net.Listen("tcp", addr)
722-
if listenErr == nil {
725+
726+
// First, try to parse the address as a file descriptor. When testing
727+
// with Caddy, the address contains a port number (even though that
728+
// doesn't really make sense for a file descriptor), so remove that too.
729+
fd, err := strconv.ParseUint(strings.Split(strings.TrimPrefix(addr, "fd/"), ":")[0], 0, strconv.IntSize)
730+
if err == nil {
731+
// os.NewFile takes ownership of the file descriptor, so if
732+
// something else is using the file descriptor, this would be bad
733+
// since it means that closing the os.File would also close the
734+
// underlying file descriptor out from under the other user. So
735+
// instead we'll duplicate the file descriptor here, then we can
736+
// safely do whatever we want with it.
737+
fd, listenErr := filedescriptor.Dup(int(fd))
738+
if listenErr != nil {
739+
continue
740+
}
741+
742+
// net.FileListener internally duplicates the file descriptor, so we
743+
// can unconditionally close the "file" after creating the listener.
744+
file := os.NewFile(uintptr(fd), addr)
745+
defer file.Close()
746+
747+
// net.FileListener will return an error if the file descriptor is
748+
// not a stream socket, so we don't need to check ourselves. But the
749+
// socket could be a Unix socket, so we still need to check for that
750+
// and raise an error if so.
751+
ln, listenErr = net.FileListener(file)
752+
if listenErr == nil {
753+
return ln, nil
754+
}
755+
756+
if ln.Addr().Network() != "tcp" {
757+
ln.Close()
758+
return nil, fmt.Errorf("file descriptor %d is not a TCP socket", fd)
759+
}
760+
723761
return ln, nil
724-
}
762+
} else {
763+
ln, listenErr = net.Listen("tcp", addr)
764+
if listenErr == nil {
765+
return ln, nil
766+
}
725767

726-
// if it failed just because the socket is already in use, we
727-
// have no choice but to assume that whatever is using the socket
728-
// can answer the challenge already, so we ignore the error
729-
connectErr := dialTCPSocket(addr)
730-
if connectErr == nil {
731-
return nil, nil
768+
// if it failed just because the socket is already in use, we
769+
// have no choice but to assume that whatever is using the socket
770+
// can answer the challenge already, so we ignore the error
771+
connectErr := dialTCPSocket(addr)
772+
if connectErr == nil {
773+
return nil, nil
774+
}
732775
}
733776

734777
// Hmm, we couldn't connect to the socket, so something else must

solvers_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
package certmagic
1616

1717
import (
18+
"net"
19+
"strconv"
1820
"testing"
1921

22+
"github.com/caddyserver/certmagic/internal/filedescriptor"
2023
"github.com/mholt/acmez/v3/acme"
2124
)
2225

@@ -177,3 +180,52 @@ func TestGetACMEChallenge_IPv6Brackets(t *testing.T) {
177180
t.Error("GetACMEChallenge(\"::1\") should find challenge stored under \"::1\"")
178181
}
179182
}
183+
184+
func TestTryListen(t *testing.T) {
185+
// Make sure that a regular TCP address still works.
186+
regularLn, err := robustTryListen("127.0.0.1:8080")
187+
if err != nil {
188+
t.Fatalf("robustTryListen with regular address: %v", err)
189+
}
190+
regularLn.Close()
191+
192+
// The rest of the tests only make sense on Unix-like systems
193+
if !filedescriptor.IsUnix {
194+
t.Skip("file descriptor tests only work on Unix-like systems")
195+
}
196+
197+
// Create a new file descriptor containing a TCP listening socket
198+
fdLn, err := net.Listen("tcp", "127.0.0.1:8080")
199+
if err != nil {
200+
t.Fatalf("net.Listen: %v", err)
201+
}
202+
defer fdLn.Close()
203+
204+
// Get the pseudo-address of the file descriptor.
205+
fd, err := fdLn.(*net.TCPListener).File()
206+
if err != nil {
207+
t.Fatalf("TCPListener.File: %v", err)
208+
}
209+
correctName := "fd/" + strconv.FormatUint(uint64(fd.Fd()), 10)
210+
211+
// Make sure that we can listen on the file descriptor.
212+
correctLn, err := robustTryListen(correctName)
213+
if err != nil {
214+
t.Fatalf("robustTryListen: %v", err)
215+
}
216+
correctLn.Close()
217+
218+
// Make sure that it still works when we add a port number.
219+
correctLn, err = robustTryListen(correctName + ":80")
220+
if err != nil {
221+
t.Fatalf("robustTryListen with port: %v", err)
222+
}
223+
correctLn.Close()
224+
225+
// Make up a fake file descriptor that shouldn't exist.
226+
fakeName := "fd/123456"
227+
_, err = robustTryListen(fakeName)
228+
if err == nil {
229+
t.Fatalf("robustTryListen(%q) should have failed", fakeName)
230+
}
231+
}

0 commit comments

Comments
 (0)