Repository: traefik/traefik (language: go)

Issue:
Add SO_REUSEPORT support for EntryPoints
### Welcome!

- [X] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [X] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you expect to see?

Somehow people never discuss [SO_REUSEPORT](https://lwn.net/Articles/542629/) here, but IMHO it can be very useful sometimes for a program like Traefik running on the very edge.

For example, if your Traefik occupies ports 80 and 443, then you have no way to gracefully upgrade or restart (currently the only way to reload the config) your Traefik without any service downtime. But with SO_REUSEPORT support, you can do canary deployments against Traefik. Just like its name, we can have two Traefik processes listen to the same TCP/UDP port (or, REUSE the same port).

[Nginx supports reuseport](https://nginx.org/en/docs/http/ngx_http_core_module.html#listen), and it served us very well:

> reuseport
> this parameter (1.9.1) instructs to create an individual listening socket for each worker process (using the SO_REUSEPORT socket option on Linux 3.9+ and DragonFly BSD, or SO_REUSEPORT_LB on FreeBSD 12+), allowing a kernel to distribute incoming connections between worker processes. This currently works only on Linux 3.9+, DragonFly BSD, and FreeBSD 12+ (1.15.1).

I think bringing SO_REUSEPORT into Traefik will be very easy, given that there are only a few `net.Listen*` calls here: https://github.com/search?q=repo%3Atraefik%2Ftraefik+language%3Ago+NOT+path%3A*_test.go+%2Fnet%5C.Listen%5Cw*%5C%28%2F&type=code.

If the Traefik Team thinks it's good but doesn't have the bandwidth, I can help draft a PR. 🙂


Discussion / hints:
Hello @aofei ,

That's an interesting idea, but as often, we would probably wait to gauge the interest of the community before spending some time on it.
So be aware that even if you open a PR, we might not examine it before we're ready to do so.

However, I am personally interested in how you would do that. In a few words, how would you change the underlying net listeners in traefik so that they have SO_REUSEPORT enabled?

thanks.
Hi @mpl!

> how would you change the underlying net listeners in traefik so that they have SO_REUSEPORT enabled?

Just to be clear we are not replacing any existing `net.Listen*` calls, they're here to stay. In other words SO_REUSEPORT should definitely not be a default enabled feature. Because even though it's kernel magic, I believe it still has a performance hit, even negligible. Also, I suggest we only add support for the Linux platform, since the Linux kernel has the best support for SO_REUSEPORT.

What's off the top of my head is that we could introduce a new configuration option for EntryPoints, maybe a boolean `static.EntryPoint.ReusePort` (defaults to `false`).

```yaml
## Static configuration
entryPoints:
  web:
    address: ":80"
    reusePort: true
```

We can then decide whether to use our own [`net.ListenConfig`](https://pkg.go.dev/net#ListenConfig) according to the `static.Traefik.ReusePort`. I believe there are only 3 main places to touch:

1. https://github.com/traefik/traefik/blob/8174860770e536b4afb541e0ab13b3611a101430/pkg/server/server_entrypoint_tcp.go#L433
2. https://github.com/traefik/traefik/blob/8174860770e536b4afb541e0ab13b3611a101430/pkg/server/server_entrypoint_tcp_http3.go#L36
3. https://github.com/traefik/traefik/blob/8174860770e536b4afb541e0ab13b3611a101430/pkg/udp/conn.go#L41

As for the actual implementation of the SO_REUSEPORT-enabled `net.ListenConfig`, it's actually quite simple (for Linux):

```go
// (I know this seems too simple to be true. But that's all the code that needs to be written.)
//
// Usage:
//     tcp80Listener, err := reusePortListenConfig.Listen(context.Background(), "tcp", "127.0.0.1:80")
//     tcp443Listener, err := reusePortListenConfig.Listen(context.Background(), "tcp", "127.0.0.1:443")
//     udp443PacketConn, err := reusePortListenConfig.ListenPacket(context.Background(), "udp", "127.0.0.1:443")
//     udp53PacketConn, err := reusePortListenConfig.ListenPacket(context.Background(), "udp", "127.0.0.1:53")
//     udp53Conn := udp53PacketConn.(*net.UDPConn)
var reusePortListenConfig = net.ListenConfig{
	Control: func(_, _ string, c syscall.RawConn) error {
		var err error
		c.Control(func(fd uintptr) {
			err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
			if err != nil {
				return
			}
			err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
			if err != nil {
				return
			}
		})
		return err
	},
}
```

Ah, the last part (3.) is what I was missing. I didn't remember/know that ListenConfig existed, thanks.

So, in 1. , you would not do a `net.Listen` anymore, but rather a `reusePortListenConfig.Listen` instead, right?

> So, in 1. , you would not do a `net.Listen` anymore, but rather a `reusePortListenConfig.Listen` instead, right?

If `static.EntryPoint.ReusePort` is `true`, then yes.

Actually the `net.Listen` is just a shorthand for `(&net.ListenConfig{}).Listen` (https://cs.opensource.google/go/go/+/refs/tags/go1.20.3:src/net/dial.go;l=707-710). So we can add a `static.EntryPoint.ListenConfig()` to make things easier (in 1, `entryPoint.ListenConfig().Listen("tcp", entryPoint.GetAddress())`). Anyway, adding the SO_REUSEPORT gizmo to Traefik won't be too invasive to the codebase.

Fix patch:
diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md
index 2f655ccb1a..d2a5db058c 100644
--- a/docs/content/reference/static-configuration/cli-ref.md
+++ b/docs/content/reference/static-configuration/cli-ref.md
@@ -180,6 +180,9 @@ Trust all. (Default: ```false```)
 `--entrypoints.<name>.proxyprotocol.trustedips`:  
 Trust only selected IPs.
 
+`--entrypoints.<name>.reuseport`:  
+Enables EntryPoints from the same or different processes listening on the same TCP/UDP port. (Default: ```false```)
+
 `--entrypoints.<name>.transport.keepalivemaxrequests`:  
 Maximum number of requests before closing a keep-alive connection. (Default: ```0```)
 
diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md
index fdf7337839..cc813b154a 100644
--- a/docs/content/reference/static-configuration/env-ref.md
+++ b/docs/content/reference/static-configuration/env-ref.md
@@ -180,6 +180,9 @@ Trust all. (Default: ```false```)
 `TRAEFIK_ENTRYPOINTS_<NAME>_PROXYPROTOCOL_TRUSTEDIPS`:  
 Trust only selected IPs.
 
+`TRAEFIK_ENTRYPOINTS_<NAME>_REUSEPORT`:  
+Enables EntryPoints from the same or different processes listening on the same TCP/UDP port. (Default: ```false```)
+
 `TRAEFIK_ENTRYPOINTS_<NAME>_TRANSPORT_KEEPALIVEMAXREQUESTS`:  
 Maximum number of requests before closing a keep-alive connection. (Default: ```0```)
 
diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml
index 8f93264167..79226cb705 100644
--- a/docs/content/reference/static-configuration/file.toml
+++ b/docs/content/reference/static-configuration/file.toml
@@ -30,6 +30,7 @@
 [entryPoints]
   [entryPoints.EntryPoint0]
     address = "foobar"
+    reusePort = true
     asDefault = true
     [entryPoints.EntryPoint0.transport]
       keepAliveMaxTime = "42s"
diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml
index 1739759cb7..45092911fe 100644
--- a/docs/content/reference/static-configuration/file.yaml
+++ b/docs/content/reference/static-configuration/file.yaml
@@ -35,6 +35,7 @@ tcpServersTransport:
 entryPoints:
   EntryPoint0:
     address: foobar
+    reusePort: true
     asDefault: true
     transport:
       lifeCycle:
diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md
index cd79c3cfd2..aa92aa5c9c 100644
--- a/docs/content/routing/entrypoints.md
+++ b/docs/content/routing/entrypoints.md
@@ -233,6 +233,79 @@ If both TCP and UDP are wanted for the same port, two entryPoints definitions ar
 
     Full details for how to specify `address` can be found in [net.Listen](https://golang.org/pkg/net/#Listen) (and [net.Dial](https://golang.org/pkg/net/#Dial)) of the doc for go.
 
+### ReusePort
+
+_Optional, Default=false_
+
+The `ReusePort` option enables EntryPoints from the same or different processes
+listening on the same TCP/UDP port by utilizing the `SO_REUSEPORT` socket option.
+It also allows the kernel to act like a load balancer to distribute incoming
+connections between entry points.
+
+For example, you can use it with the [transport.lifeCycle](#lifecycle) to do
+canary deployments against Traefik itself. Like upgrading Traefik version or
+reloading the static configuration without any service downtime.
+
+!!! warning "Supported platforms"
+
+    The `ReusePort` option currently works only on Linux, FreeBSD, OpenBSD and Darwin.
+    It will be ignored on other platforms.
+
+    There is a known bug in the Linux kernel that may cause unintended TCP connection failures when using the `ReusePort` option.
+    For more details, see https://lwn.net/Articles/853637/.
+
+??? example "Listen on the same port"
+
+    ```yaml tab="File (yaml)"
+    entryPoints:
+      web:
+        address: ":80"
+        reusePort: true
+    ```
+
+    ```toml tab="File (TOML)"
+    [entryPoints.web]
+      address = ":80"
+      reusePort = true
+    ```
+
+    ```bash tab="CLI"
+    --entrypoints.web.address=:80
+    --entrypoints.web.reusePort=true
+    ```
+
+    Now it is possible to run multiple Traefik processes with the same EntryPoint configuration.
+
+??? example "Listen on the same port but bind to a different host"
+
+    ```yaml tab="File (yaml)"
+    entryPoints:
+      web:
+        address: ":80"
+        reusePort: true
+      privateWeb:
+        address: "192.168.1.2:80"
+        reusePort: true
+    ```
+
+    ```toml tab="File (TOML)"
+    [entryPoints.web]
+      address = ":80"
+      reusePort = true
+    [entryPoints.privateWeb]
+      address = "192.168.1.2:80"
+      reusePort = true
+    ```
+
+    ```bash tab="CLI"
+    --entrypoints.web.address=:80
+    --entrypoints.web.reusePort=true
+    --entrypoints.privateWeb.address=192.168.1.2:80
+    --entrypoints.privateWeb.reusePort=true
+    ```
+
+    Requests to `192.168.1.2:80` will only be handled by routers that have `privateWeb` as the entry point.
+
 ### AsDefault
 
 _Optional, Default=false_
diff --git a/go.mod b/go.mod
index 3685c0c5a7..b4e712b51d 100644
--- a/go.mod
+++ b/go.mod
@@ -80,6 +80,7 @@ require (
 	golang.org/x/exp v0.0.0-20231006140011-7918f672742d
 	golang.org/x/mod v0.13.0
 	golang.org/x/net v0.17.0
+	golang.org/x/sys v0.15.0
 	golang.org/x/text v0.13.0
 	golang.org/x/time v0.3.0
 	golang.org/x/tools v0.14.0
@@ -315,7 +316,6 @@ require (
 	golang.org/x/arch v0.4.0 // indirect
 	golang.org/x/crypto v0.14.0 // indirect
 	golang.org/x/oauth2 v0.13.0 // indirect
-	golang.org/x/sys v0.15.0 // indirect
 	golang.org/x/term v0.13.0 // indirect
 	google.golang.org/api v0.128.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go
index 2c1187ac59..d7b155dbe0 100644
--- a/pkg/config/static/entrypoints.go
+++ b/pkg/config/static/entrypoints.go
@@ -12,6 +12,7 @@ import (
 // EntryPoint holds the entry point configuration.
 type EntryPoint struct {
 	Address          string                `description:"Entry point address." json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"`
+	ReusePort        bool                  `description:"Enables EntryPoints from the same or different processes listening on the same TCP/UDP port." json:"reusePort,omitempty" toml:"reusePort,omitempty" yaml:"reusePort,omitempty"`
 	AsDefault        bool                  `description:"Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined." json:"asDefault,omitempty" toml:"asDefault,omitempty" yaml:"asDefault,omitempty"`
 	Transport        *EntryPointsTransport `description:"Configures communication between clients and Traefik." json:"transport,omitempty" toml:"transport,omitempty" yaml:"transport,omitempty" export:"true"`
 	ProxyProtocol    *ProxyProtocol        `description:"Proxy-Protocol configuration." json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
diff --git a/pkg/server/server_entrypoint_listenconfig_other.go b/pkg/server/server_entrypoint_listenconfig_other.go
new file mode 100644
index 0000000000..1990122843
--- /dev/null
+++ b/pkg/server/server_entrypoint_listenconfig_other.go
@@ -0,0 +1,15 @@
+//go:build !(linux || freebsd || openbsd || darwin)
+
+package server
+
+import (
+	"net"
+
+	"github.com/traefik/traefik/v3/pkg/config/static"
+)
+
+// newListenConfig creates a new net.ListenConfig for the given configuration of
+// the entry point.
+func newListenConfig(configuration *static.EntryPoint) (lc net.ListenConfig) {
+	return
+}
diff --git a/pkg/server/server_entrypoint_listenconfig_unix.go b/pkg/server/server_entrypoint_listenconfig_unix.go
new file mode 100644
index 0000000000..7e43f4f7d9
--- /dev/null
+++ b/pkg/server/server_entrypoint_listenconfig_unix.go
@@ -0,0 +1,44 @@
+//go:build linux || freebsd || openbsd || darwin
+
+package server
+
+import (
+	"fmt"
+	"net"
+	"syscall"
+
+	"github.com/traefik/traefik/v3/pkg/config/static"
+	"golang.org/x/sys/unix"
+)
+
+// newListenConfig creates a new net.ListenConfig for the given configuration of
+// the entry point.
+func newListenConfig(configuration *static.EntryPoint) (lc net.ListenConfig) {
+	if configuration != nil && configuration.ReusePort {
+		lc.Control = controlReusePort
+	}
+	return
+}
+
+// controlReusePort is a net.ListenConfig.Control function that enables SO_REUSEPORT
+// on the socket.
+func controlReusePort(network, address string, c syscall.RawConn) error {
+	var setSockOptErr error
+	err := c.Control(func(fd uintptr) {
+		// Note that net.ListenConfig enables unix.SO_REUSEADDR by default,
+		// as seen in https://go.dev/src/net/sockopt_linux.go. Therefore, no
+		// additional action is required to enable it here.
+
+		setSockOptErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unixSOREUSEPORT, 1)
+		if setSockOptErr != nil {
+			return
+		}
+	})
+	if err != nil {
+		return fmt.Errorf("control: %w", err)
+	}
+	if setSockOptErr != nil {
+		return fmt.Errorf("setsockopt: %w", setSockOptErr)
+	}
+	return nil
+}
diff --git a/pkg/server/server_entrypoint_listenconfig_unix_sockopt_freebsd.go b/pkg/server/server_entrypoint_listenconfig_unix_sockopt_freebsd.go
new file mode 100644
index 0000000000..e20b61c48b
--- /dev/null
+++ b/pkg/server/server_entrypoint_listenconfig_unix_sockopt_freebsd.go
@@ -0,0 +1,7 @@
+//go:build freebsd
+
+package server
+
+import "golang.org/x/sys/unix"
+
+const unixSOREUSEPORT = unix.SO_REUSEPORT_LB
diff --git a/pkg/server/server_entrypoint_listenconfig_unix_sockopt_other.go b/pkg/server/server_entrypoint_listenconfig_unix_sockopt_other.go
new file mode 100644
index 0000000000..306dd8bd24
--- /dev/null
+++ b/pkg/server/server_entrypoint_listenconfig_unix_sockopt_other.go
@@ -0,0 +1,7 @@
+//go:build linux || openbsd || darwin
+
+package server
+
+import "golang.org/x/sys/unix"
+
+const unixSOREUSEPORT = unix.SO_REUSEPORT
diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go
index e4d2892a86..c7d33abb7a 100644
--- a/pkg/server/server_entrypoint_tcp.go
+++ b/pkg/server/server_entrypoint_tcp.go
@@ -460,7 +460,8 @@ func buildProxyProtocolListener(ctx context.Context, entryPoint *static.EntryPoi
 }
 
 func buildListener(ctx context.Context, entryPoint *static.EntryPoint) (net.Listener, error) {
-	listener, err := net.Listen("tcp", entryPoint.GetAddress())
+	listenConfig := newListenConfig(entryPoint)
+	listener, err := listenConfig.Listen(ctx, "tcp", entryPoint.GetAddress())
 	if err != nil {
 		return nil, fmt.Errorf("error opening listener: %w", err)
 	}
diff --git a/pkg/server/server_entrypoint_tcp_http3.go b/pkg/server/server_entrypoint_tcp_http3.go
index 188be80263..827d6638b7 100644
--- a/pkg/server/server_entrypoint_tcp_http3.go
+++ b/pkg/server/server_entrypoint_tcp_http3.go
@@ -33,7 +33,8 @@ func newHTTP3Server(ctx context.Context, configuration *static.EntryPoint, https
 		return nil, errors.New("advertised port must be greater than or equal to zero")
 	}
 
-	conn, err := net.ListenPacket("udp", configuration.GetAddress())
+	listenConfig := newListenConfig(configuration)
+	conn, err := listenConfig.ListenPacket(ctx, "udp", configuration.GetAddress())
 	if err != nil {
 		return nil, fmt.Errorf("starting listener: %w", err)
 	}
diff --git a/pkg/server/server_entrypoint_udp.go b/pkg/server/server_entrypoint_udp.go
index 347b169db2..08ed4fb1f6 100644
--- a/pkg/server/server_entrypoint_udp.go
+++ b/pkg/server/server_entrypoint_udp.go
@@ -3,7 +3,6 @@ package server
 import (
 	"context"
 	"fmt"
-	"net"
 	"sync"
 	"time"
 
@@ -87,12 +86,8 @@ type UDPEntryPoint struct {
 
 // NewUDPEntryPoint returns a UDP entry point.
 func NewUDPEntryPoint(cfg *static.EntryPoint) (*UDPEntryPoint, error) {
-	addr, err := net.ResolveUDPAddr("udp", cfg.GetAddress())
-	if err != nil {
-		return nil, err
-	}
-
-	listener, err := udp.Listen("udp", addr, time.Duration(cfg.UDP.Timeout))
+	listenConfig := newListenConfig(cfg)
+	listener, err := udp.Listen(listenConfig, "udp", cfg.GetAddress(), time.Duration(cfg.UDP.Timeout))
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/udp/conn.go b/pkg/udp/conn.go
index 5753a9c135..6b6054b69f 100644
--- a/pkg/udp/conn.go
+++ b/pkg/udp/conn.go
@@ -1,7 +1,9 @@
 package udp
 
 import (
+	"context"
 	"errors"
+	"fmt"
 	"io"
 	"net"
 	"sync"
@@ -33,18 +35,22 @@ type Listener struct {
 }
 
 // Listen creates a new listener.
-func Listen(network string, laddr *net.UDPAddr, timeout time.Duration) (*Listener, error) {
+func Listen(listenConfig net.ListenConfig, network, address string, timeout time.Duration) (*Listener, error) {
 	if timeout <= 0 {
 		return nil, errors.New("timeout should be greater than zero")
 	}
 
-	conn, err := net.ListenUDP(network, laddr)
+	packetConn, err := listenConfig.ListenPacket(context.Background(), network, address)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("listen packet: %w", err)
+	}
+	pConn, ok := packetConn.(*net.UDPConn)
+	if !ok {
+		return nil, errors.New("packet conn is not an UDPConn")
 	}
 
 	l := &Listener{
-		pConn:     conn,
+		pConn:     pConn,
 		acceptCh:  make(chan *Conn),
 		conns:     make(map[string]*Conn),
 		accepting: true,


Test patch:
diff --git a/pkg/server/server_entrypoint_listenconfig_other_test.go b/pkg/server/server_entrypoint_listenconfig_other_test.go
new file mode 100644
index 0000000000..f2c736d260
--- /dev/null
+++ b/pkg/server/server_entrypoint_listenconfig_other_test.go
@@ -0,0 +1,44 @@
+//go:build !(linux || freebsd || openbsd || darwin)
+
+package server
+
+import (
+	"context"
+	"net"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"github.com/traefik/traefik/v3/pkg/config/static"
+)
+
+func TestNewListenConfig(t *testing.T) {
+	ep := static.EntryPoint{Address: ":0"}
+	listenConfig := newListenConfig(&ep)
+	require.Nil(t, listenConfig.Control)
+	require.Zero(t, listenConfig.KeepAlive)
+
+	l1, err := listenConfig.Listen(context.Background(), "tcp", ep.Address)
+	require.NoError(t, err)
+	require.NotNil(t, l1)
+	defer l1.Close()
+
+	l2, err := listenConfig.Listen(context.Background(), "tcp", l1.Addr().String())
+	require.Error(t, err)
+	require.ErrorContains(t, err, "address already in use")
+	require.Nil(t, l2)
+
+	ep = static.EntryPoint{Address: ":0", ReusePort: true}
+	listenConfig = newListenConfig(&ep)
+	require.Nil(t, listenConfig.Control)
+	require.Zero(t, listenConfig.KeepAlive)
+
+	l3, err := listenConfig.Listen(context.Background(), "tcp", ep.Address)
+	require.NoError(t, err)
+	require.NotNil(t, l3)
+	defer l3.Close()
+
+	l4, err := listenConfig.Listen(context.Background(), "tcp", l3.Addr().String())
+	require.Error(t, err)
+	require.ErrorContains(t, err, "address already in use")
+	require.Nil(t, l4)
+}
diff --git a/pkg/server/server_entrypoint_listenconfig_unix_test.go b/pkg/server/server_entrypoint_listenconfig_unix_test.go
new file mode 100644
index 0000000000..a5f7dda0a6
--- /dev/null
+++ b/pkg/server/server_entrypoint_listenconfig_unix_test.go
@@ -0,0 +1,56 @@
+//go:build linux || freebsd || openbsd || darwin
+
+package server
+
+import (
+	"context"
+	"net"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"github.com/traefik/traefik/v3/pkg/config/static"
+)
+
+func TestNewListenConfig(t *testing.T) {
+	ep := static.EntryPoint{Address: ":0"}
+	listenConfig := newListenConfig(&ep)
+	require.Nil(t, listenConfig.Control)
+	require.Zero(t, listenConfig.KeepAlive)
+
+	l1, err := listenConfig.Listen(context.Background(), "tcp", ep.Address)
+	require.NoError(t, err)
+	require.NotNil(t, l1)
+	defer l1.Close()
+
+	l2, err := listenConfig.Listen(context.Background(), "tcp", l1.Addr().String())
+	require.Error(t, err)
+	require.ErrorContains(t, err, "address already in use")
+	require.Nil(t, l2)
+
+	ep = static.EntryPoint{Address: ":0", ReusePort: true}
+	listenConfig = newListenConfig(&ep)
+	require.NotNil(t, listenConfig.Control)
+	require.Zero(t, listenConfig.KeepAlive)
+
+	l3, err := listenConfig.Listen(context.Background(), "tcp", ep.Address)
+	require.NoError(t, err)
+	require.NotNil(t, l3)
+	defer l3.Close()
+
+	l4, err := listenConfig.Listen(context.Background(), "tcp", l3.Addr().String())
+	require.NoError(t, err)
+	require.NotNil(t, l4)
+	defer l4.Close()
+
+	_, l3Port, err := net.SplitHostPort(l3.Addr().String())
+	require.NoError(t, err)
+	l5, err := listenConfig.Listen(context.Background(), "tcp", "127.0.0.1:"+l3Port)
+	require.NoError(t, err)
+	require.NotNil(t, l5)
+	defer l5.Close()
+
+	l6, err := listenConfig.Listen(context.Background(), "tcp", l1.Addr().String())
+	require.Error(t, err)
+	require.ErrorContains(t, err, "address already in use")
+	require.Nil(t, l6)
+}
diff --git a/pkg/udp/conn_test.go b/pkg/udp/conn_test.go
index 44e7484744..3c8cc0a7fd 100644
--- a/pkg/udp/conn_test.go
+++ b/pkg/udp/conn_test.go
@@ -14,10 +14,7 @@ import (
 )
 
 func TestConsecutiveWrites(t *testing.T) {
-	addr, err := net.ResolveUDPAddr("udp", ":0")
-	require.NoError(t, err)
-
-	ln, err := Listen("udp", addr, 3*time.Second)
+	ln, err := Listen(net.ListenConfig{}, "udp", ":0", 3*time.Second)
 	require.NoError(t, err)
 	defer func() {
 		err := ln.Close()
@@ -75,11 +72,7 @@ func TestConsecutiveWrites(t *testing.T) {
 }
 
 func TestListenNotBlocking(t *testing.T) {
-	addr, err := net.ResolveUDPAddr("udp", ":0")
-
-	require.NoError(t, err)
-
-	ln, err := Listen("udp", addr, 3*time.Second)
+	ln, err := Listen(net.ListenConfig{}, "udp", ":0", 3*time.Second)
 	require.NoError(t, err)
 	defer func() {
 		err := ln.Close()
@@ -165,10 +158,7 @@ func TestListenNotBlocking(t *testing.T) {
 }
 
 func TestListenWithZeroTimeout(t *testing.T) {
-	addr, err := net.ResolveUDPAddr("udp", ":0")
-	require.NoError(t, err)
-
-	_, err = Listen("udp", addr, 0)
+	_, err := Listen(net.ListenConfig{}, "udp", ":0", 0)
 	assert.Error(t, err)
 }
 
@@ -183,10 +173,7 @@ func TestTimeoutWithoutRead(t *testing.T) {
 func testTimeout(t *testing.T, withRead bool) {
 	t.Helper()
 
-	addr, err := net.ResolveUDPAddr("udp", ":0")
-	require.NoError(t, err)
-
-	ln, err := Listen("udp", addr, 3*time.Second)
+	ln, err := Listen(net.ListenConfig{}, "udp", ":0", 3*time.Second)
 	require.NoError(t, err)
 	defer func() {
 		err := ln.Close()
@@ -227,10 +214,7 @@ func testTimeout(t *testing.T, withRead bool) {
 }
 
 func TestShutdown(t *testing.T) {
-	addr, err := net.ResolveUDPAddr("udp", ":0")
-	require.NoError(t, err)
-
-	l, err := Listen("udp", addr, 3*time.Second)
+	l, err := Listen(net.ListenConfig{}, "udp", ":0", 3*time.Second)
 	require.NoError(t, err)
 
 	go func() {
@@ -331,10 +315,7 @@ func TestReadLoopMaxDataSize(t *testing.T) {
 
 	doneCh := make(chan struct{})
 
-	addr, err := net.ResolveUDPAddr("udp", ":0")
-	require.NoError(t, err)
-
-	l, err := Listen("udp", addr, 3*time.Second)
+	l, err := Listen(net.ListenConfig{}, "udp", ":0", 3*time.Second)
 	require.NoError(t, err)
 
 	defer func() {
diff --git a/pkg/udp/proxy_test.go b/pkg/udp/proxy_test.go
index b3ce2ec2c0..3b57038754 100644
--- a/pkg/udp/proxy_test.go
+++ b/pkg/udp/proxy_test.go
@@ -96,10 +96,7 @@ func TestProxy_ServeUDP_MaxDataSize(t *testing.T) {
 func newServer(t *testing.T, addr string, handler Handler) {
 	t.Helper()
 
-	addrL, err := net.ResolveUDPAddr("udp", addr)
-	require.NoError(t, err)
-
-	listener, err := Listen("udp", addrL, 3*time.Second)
+	listener, err := Listen(net.ListenConfig{}, "udp", addr, 3*time.Second)
 	require.NoError(t, err)
 
 	for {