mirror of
https://github.com/caddyserver/caddy.git
synced 2026-04-29 20:20:40 -04:00
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Waiting to run
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Waiting to run
Lint / lint (macos-14, mac) (push) Waiting to run
Lint / lint (windows-latest, windows) (push) Waiting to run
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m35s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Failing after 1m52s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 3m33s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 3m49s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 4m0s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m6s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Failing after 2m36s
Lint / lint (ubuntu-latest, linux) (push) Failing after 2m5s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m41s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m31s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m26s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m31s
Lint / dependency-review (push) Failing after 1m2s
Lint / govulncheck (push) Failing after 2m35s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m54s
* Delete old unix domain socket files on Windows While Windows doesn't have the need to reuse a socket file descriptor by dup()ing it on config reloads, there still is a valid need for an equivalent to the `syscall.Unlink()` call in listen_unix.go (also in `reuseUnixSocket`). If a previous Caddy instance didn't terminate properly, the chances it will leave behind a socket file are very high, breaking all subsequent starting attempts. Other than for regular files, Windows seemingly has no way for a process to flag a UNIX domain socket file with `FILE_DELETE_ON_CLOSE`, which means this scenario can never be avoided entirely (e.g. in the case of crashes). For the long comment on `isAbstractUnixSocket`: the logic itself is likely of dubious value, but I thought it better to explicitly reference the issue, as I have just spent half an hour searching the web to figure out whether abstract names will work or not on Windows. At least, the logic as-is should now do the sensible thing if these are ever implemented properly (and it matches what the Golang standard library does internally). * Add a dial attempt to check for active server processes As @steadytao pointed out (thanks!), the previous code didn't have solid proof that an existing unix socket file had really been orphaned, as it's also possible that there's another server process (still running). This would still give the Windows implementation parity with the unix one (as that one also unlinks the socket file without further checks), but I've performed a couple of small tests and found this way of handling socket files still problematic at least problematic if Caddy is used as a reverse proxy in real world scenarios. In tests with a simple Caddyfile that only declares an admin socket, starting two caddy instances with the same Caddyfile works and behaves like one would expect: the second instance removes the first instance's socket file and "wins" the race. When Caddy is used as a reverse proxy, though, what'll happen is more complicated: While the second instance wins the race for the admin socket, as long as the Caddyfile specifies a TCP downstream socket, the second process will not be able to take this one over from the first (also to be expected, that's how socket binding usually works). This results in a rather broken state: The first process still holds on to its TCP listening sockets, the second process fails to start because of the error in its listening attempt, leaving an orphaned admin socket file in the file system. Afterwards, the second process won't be running and the first _will_ be running but unable to be controlled because its admin socket has been replaced. This leaves the system in another state that is bad from an ops perspective. With this new change, we try first to connect to any unix socket that isn't already covered by our current process (with a very low timeout) and can easily decide if the socket is still in use by another process: - If the connection is accepted, there's obviously a server process. - If Windows returns WSACONNREFUSED [^1], there is either no active server process for the socket file anymore, or the socket file does not exist. - Any other errors are likely a sign that there still is a server process (e.g. a timeout would indicate that it's just slow in accepting new connection attempts). [^1]: https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2#wsaeconnrefused * chore: tidy Windows unix socket reuse helper --------- Co-authored-by: Zen Dodd <mail@steadytao.com>
311 lines
9.2 KiB
Go
311 lines
9.2 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//go:build !unix || solaris
|
|
|
|
package caddy
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"slices"
|
|
"strconv"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
|
|
var socketFile *os.File
|
|
|
|
fd := slices.Contains([]string{"fd", "fdgram"}, network)
|
|
if fd {
|
|
socketFd, err := strconv.ParseUint(address, 0, strconv.IntSize)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid file descriptor: %v", err)
|
|
}
|
|
|
|
func() {
|
|
socketFilesMu.Lock()
|
|
defer socketFilesMu.Unlock()
|
|
|
|
socketFdWide := uintptr(socketFd)
|
|
var ok bool
|
|
|
|
socketFile, ok = socketFiles[socketFdWide]
|
|
|
|
if !ok {
|
|
socketFile = os.NewFile(socketFdWide, lnKey)
|
|
if socketFile != nil {
|
|
socketFiles[socketFdWide] = socketFile
|
|
}
|
|
}
|
|
}()
|
|
|
|
if socketFile == nil {
|
|
return nil, fmt.Errorf("invalid socket file descriptor: %d", socketFd)
|
|
}
|
|
}
|
|
|
|
datagram := slices.Contains([]string{"udp", "udp4", "udp6", "unixgram", "fdgram"}, network)
|
|
if datagram {
|
|
sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
|
var (
|
|
pc net.PacketConn
|
|
err error
|
|
)
|
|
if fd {
|
|
pc, err = net.FilePacketConn(socketFile)
|
|
} else {
|
|
pc, err = config.ListenPacket(ctx, network, address)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil
|
|
}
|
|
|
|
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
|
var (
|
|
ln net.Listener
|
|
err error
|
|
)
|
|
if fd {
|
|
ln, err = net.FileListener(socketFile)
|
|
} else {
|
|
ln, err = config.Listen(ctx, network, address)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &sharedListener{Listener: ln, key: lnKey}, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAliveConfig: config.KeepAliveConfig}, nil
|
|
}
|
|
|
|
// fakeCloseListener is a private wrapper over a listener that
|
|
// is shared. The state of fakeCloseListener is not shared.
|
|
// This allows one user of a socket to "close" the listener
|
|
// while in reality the socket stays open for other users of
|
|
// the listener. In this way, servers become hot-swappable
|
|
// while the listener remains running. Listeners should be
|
|
// re-wrapped in a new fakeCloseListener each time the listener
|
|
// is reused. This type is atomic and values must not be copied.
|
|
type fakeCloseListener struct {
|
|
closed atomic.Bool
|
|
*sharedListener // embedded, so we also become a net.Listener
|
|
keepAliveConfig net.KeepAliveConfig
|
|
}
|
|
|
|
type canSetKeepAliveConfig interface {
|
|
SetKeepAliveConfig(config net.KeepAliveConfig) error
|
|
}
|
|
|
|
func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
|
|
// if the listener is already "closed", return error
|
|
if fcl.closed.Load() {
|
|
return nil, fakeClosedErr(fcl)
|
|
}
|
|
|
|
// call underlying accept
|
|
conn, err := fcl.sharedListener.Accept()
|
|
if err == nil {
|
|
// if 0, do nothing, Go's default is already set
|
|
// and if the connection allows setting KeepAlive, set it
|
|
if tconn, ok := conn.(canSetKeepAliveConfig); ok && fcl.keepAliveConfig.Enable {
|
|
err = tconn.SetKeepAliveConfig(fcl.keepAliveConfig)
|
|
if err != nil {
|
|
Log().With(zap.String("server", fcl.sharedListener.key)).Warn("unable to set keepalive for new connection:", zap.Error(err))
|
|
}
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
// since Accept() returned an error, it may be because our reference to
|
|
// the listener (this fakeCloseListener) may have been closed, i.e. the
|
|
// server is shutting down; in that case, we need to clear the deadline
|
|
// that we set when Close() was called, and return a non-temporary and
|
|
// non-timeout error value to the caller, masking the "true" error, so
|
|
// that server loops / goroutines won't retry, linger, and leak
|
|
if fcl.closed.Load() {
|
|
// we dereference the sharedListener explicitly even though it's embedded
|
|
// so that it's clear in the code that side-effects are shared with other
|
|
// users of this listener, not just our own reference to it; we also don't
|
|
// do anything with the error because all we could do is log it, but we
|
|
// explicitly assign it to nothing so we don't forget it's there if needed
|
|
_ = fcl.sharedListener.clearDeadline()
|
|
|
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
|
return nil, fakeClosedErr(fcl)
|
|
}
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
// Close stops accepting new connections without closing the
|
|
// underlying listener. The underlying listener is only closed
|
|
// if the caller is the last known user of the socket.
|
|
func (fcl *fakeCloseListener) Close() error {
|
|
if fcl.closed.CompareAndSwap(false, true) {
|
|
// There are two ways I know of to get an Accept()
|
|
// function to return to the server loop that called
|
|
// it: close the listener, or set a deadline in the
|
|
// past. Obviously, we can't close the socket yet
|
|
// since others may be using it (hence this whole
|
|
// file). But we can set the deadline in the past,
|
|
// and this is kind of cheating, but it works, and
|
|
// it apparently even works on Windows.
|
|
_ = fcl.sharedListener.setDeadline()
|
|
_, _ = listenerPool.Delete(fcl.sharedListener.key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sharedListener is a wrapper over an underlying listener. The listener
|
|
// and the other fields on the struct are shared state that is synchronized,
|
|
// so sharedListener structs must never be copied (always use a pointer).
|
|
type sharedListener struct {
|
|
net.Listener
|
|
key string // uniquely identifies this listener
|
|
deadline bool // whether a deadline is currently set
|
|
deadlineMu sync.Mutex
|
|
}
|
|
|
|
func (sl *sharedListener) clearDeadline() error {
|
|
var err error
|
|
sl.deadlineMu.Lock()
|
|
if sl.deadline {
|
|
switch ln := sl.Listener.(type) {
|
|
case *net.TCPListener:
|
|
err = ln.SetDeadline(time.Time{})
|
|
}
|
|
sl.deadline = false
|
|
}
|
|
sl.deadlineMu.Unlock()
|
|
return err
|
|
}
|
|
|
|
func (sl *sharedListener) setDeadline() error {
|
|
timeInPast := time.Now().Add(-1 * time.Minute)
|
|
var err error
|
|
sl.deadlineMu.Lock()
|
|
if !sl.deadline {
|
|
switch ln := sl.Listener.(type) {
|
|
case *net.TCPListener:
|
|
err = ln.SetDeadline(timeInPast)
|
|
}
|
|
sl.deadline = true
|
|
}
|
|
sl.deadlineMu.Unlock()
|
|
return err
|
|
}
|
|
|
|
// Destruct is called by the UsagePool when the listener is
|
|
// finally not being used anymore. It closes the socket.
|
|
func (sl *sharedListener) Destruct() error {
|
|
return sl.Listener.Close()
|
|
}
|
|
|
|
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
|
|
// or more specifically, *net.UDPConn
|
|
type fakeClosePacketConn struct {
|
|
closed atomic.Bool
|
|
*sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
|
|
}
|
|
|
|
func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
|
// if the listener is already "closed", return error
|
|
if fcpc.closed.Load() {
|
|
return 0, nil, &net.OpError{
|
|
Op: "readfrom",
|
|
Net: fcpc.LocalAddr().Network(),
|
|
Addr: fcpc.LocalAddr(),
|
|
Err: errFakeClosed,
|
|
}
|
|
}
|
|
|
|
// call underlying readfrom
|
|
n, addr, err = fcpc.sharedPacketConn.ReadFrom(p)
|
|
if err != nil {
|
|
// this server was stopped, so clear the deadline and let
|
|
// any new server continue reading; but we will exit
|
|
if fcpc.closed.Load() {
|
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
|
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
|
|
return n, addr, err
|
|
}
|
|
}
|
|
}
|
|
return n, addr, err
|
|
}
|
|
|
|
return n, addr, err
|
|
}
|
|
|
|
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
|
|
func (fcpc *fakeClosePacketConn) Close() error {
|
|
if fcpc.closed.CompareAndSwap(false, true) {
|
|
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
|
|
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (fcpc *fakeClosePacketConn) Unwrap() net.PacketConn {
|
|
return fcpc.sharedPacketConn.PacketConn
|
|
}
|
|
|
|
// sharedPacketConn is like sharedListener, but for net.PacketConns.
|
|
type sharedPacketConn struct {
|
|
net.PacketConn
|
|
key string
|
|
}
|
|
|
|
// Destruct closes the underlying socket.
|
|
func (spc *sharedPacketConn) Destruct() error {
|
|
return spc.PacketConn.Close()
|
|
}
|
|
|
|
// Unwrap returns the underlying socket
|
|
func (spc *sharedPacketConn) Unwrap() net.PacketConn {
|
|
return spc.PacketConn
|
|
}
|
|
|
|
// Interface guards (see https://github.com/caddyserver/caddy/issues/3998)
|
|
var (
|
|
_ (interface {
|
|
Unwrap() net.PacketConn
|
|
}) = (*fakeClosePacketConn)(nil)
|
|
)
|
|
|
|
// socketFiles is a fd -> *os.File map used to make a FileListener/FilePacketConn from a socket file descriptor.
|
|
var socketFiles = map[uintptr]*os.File{}
|
|
|
|
// socketFilesMu synchronizes socketFiles insertions
|
|
var socketFilesMu sync.Mutex
|