libnet/d/bridge: pass SCTP sock to the proxy

Since commit b3fabedec, the bridge driver maps ports following a 3-step
process: 1. create a socket, and bind it to the host port; 2. create
iptables rules; 3. start the userland proxy (if it's enabled). This
ensures that the port is really free before inserting iptables rules
that could otherwise disrupt host services.

However, this 3-step process wasn't implemented for SCTP, because we had
no way to instiantiate an SCTP listener from an fd. Since
github.com/ishidawataru/sctp@4719921f9, we can.

Signed-off-by: Albin Kerouanton <albinker@gmail.com>
This commit is contained in:
Albin Kerouanton
2025-07-02 00:15:23 +02:00
parent c833bd598e
commit b5bf89c315
3 changed files with 125 additions and 49 deletions

View File

@@ -1,7 +1,6 @@
package main
import (
"errors"
"flag"
"fmt"
"net"
@@ -128,18 +127,18 @@ func newProxy(config ProxyConfig) (p Proxy, err error) {
p, err = NewUDPProxy(listener, container, ipv)
case "sctp":
var listener *sctp.SCTPListener
if config.ListenSock != nil {
// There's no way to construct an SCTPListener from a file descriptor at the moment.
// If a socket has been passed in, it's probably from a newer daemon using a version
// of the sctp module that does allow it.
return nil, errors.New("cannot use supplied SCTP socket, check the latest docker-proxy is in your $PATH")
if config.ListenSock == nil {
hostAddr := &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: config.HostIP}}, Port: config.HostPort}
listener, err = sctp.ListenSCTP("sctp"+string(ipv), hostAddr)
if err != nil {
return nil, fmt.Errorf("failed to listen on %s: %w", hostAddr, err)
}
} else {
if listener, err = sctp.FileListener(config.ListenSock); err != nil {
return nil, err
}
}
hostAddr := &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: config.HostIP}}, Port: config.HostPort}
container := &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: config.ContainerIP}}, Port: config.ContainerPort}
listener, err = sctp.ListenSCTP("sctp"+string(ipv), hostAddr)
if err != nil {
return nil, fmt.Errorf("failed to listen on %s: %w", hostAddr, err)
}
p, err = NewSCTPProxy(listener, container)
default:
return nil, fmt.Errorf("unsupported protocol %s", config.Proto)
@@ -179,7 +178,11 @@ func parseFlags() ProxyConfig {
}
if useListenFd {
_ = syscall.SetNonblock(int(listenSockFd), true)
// Unlike the stdlib, passing a non-blocking socket to `sctp.FileListener`
// will result in a non-blocking Accept(). So, do not set this flag for SCTP.
if config.Proto != "sctp" {
_ = syscall.SetNonblock(int(listenSockFd), true)
}
config.ListenSock = os.NewFile(listenSockFd, "listen-sock")
}

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/ishidawataru/sctp"
"golang.org/x/sys/unix"
"gotest.tools/v3/assert"
)
@@ -155,6 +156,65 @@ func udpListener(t *testing.T, nw string, addr *net.UDPAddr) (*os.File, *net.UDP
return osFile, l.LocalAddr().(*net.UDPAddr)
}
func sctpListener(t *testing.T, nw string, addr *sctp.SCTPAddr) (*os.File, *sctp.SCTPAddr) {
t.Helper()
var domain int
var sa unix.Sockaddr
switch nw {
case "sctp4":
domain = unix.AF_INET
sa = &unix.SockaddrInet4{
Addr: [4]uint8(addr.IPAddrs[0].IP.To4()),
Port: addr.Port,
}
case "sctp6":
domain = unix.AF_INET6
sa = &unix.SockaddrInet6{
Addr: [16]uint8(addr.IPAddrs[0].IP.To16()),
Port: addr.Port,
}
default:
t.Fatalf("unknown SCTP network type: %s", nw)
}
sockfd, err := unix.Socket(domain, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, unix.IPPROTO_SCTP)
assert.NilError(t, err)
err = unix.Bind(sockfd, sa)
assert.NilError(t, err)
err = unix.Listen(sockfd, -1)
assert.NilError(t, err)
newfd, _, sysErr := unix.Syscall(unix.SYS_FCNTL, uintptr(sockfd), unix.F_DUPFD_CLOEXEC, 0)
if sysErr != 0 {
t.Fatal(os.NewSyscallError("fcntl", sysErr))
}
err = unix.Close(sockfd)
assert.NilError(t, err)
sockname, err := unix.Getsockname(int(newfd))
assert.NilError(t, err)
var laddr *sctp.SCTPAddr
switch sa := sockname.(type) {
case *unix.SockaddrInet4:
laddr = &sctp.SCTPAddr{
IPAddrs: []net.IPAddr{{IP: sa.Addr[:]}},
Port: sa.Port,
}
case *unix.SockaddrInet6:
laddr = &sctp.SCTPAddr{
IPAddrs: []net.IPAddr{{IP: sa.Addr[:]}},
Port: sa.Port,
}
}
return os.NewFile(newfd, ""), laddr
}
func testProxyAt(t *testing.T, proto string, proxy Proxy, addr string, halfClose bool) {
t.Helper()
defer proxy.Close()
@@ -414,3 +474,41 @@ func TestSCTP6ProxyNoListener(t *testing.T) {
assert.NilError(t, err)
testProxyAt(t, "sctp", proxy, fmt.Sprintf("[%s]:%d", config.HostIP, config.HostPort), false)
}
func TestSCTP4Proxy(t *testing.T) {
backend := NewEchoServer(t, "sctp", "127.0.0.1:0", EchoServerOptions{})
defer backend.Close()
backend.Run()
listener, frontendAddr := sctpListener(t, "sctp4", &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: net.ParseIP("127.0.0.1")}}, Port: 0})
backendAddr := backend.LocalAddr().(*sctp.SCTPAddr)
config := ProxyConfig{
Proto: "sctp",
HostIP: frontendAddr.IPAddrs[0].IP,
HostPort: frontendAddr.Port,
ContainerIP: backendAddr.IPAddrs[0].IP,
ContainerPort: backendAddr.Port,
ListenSock: listener,
}
proxy, err := newProxy(config)
assert.NilError(t, err)
testProxyAt(t, "sctp", proxy, fmt.Sprintf("%s:%d", config.HostIP, config.HostPort), false)
}
func TestSCTP6Proxy(t *testing.T) {
backend := NewEchoServer(t, "sctp", "[::1]:0", EchoServerOptions{})
defer backend.Close()
backend.Run()
listener, frontendAddr := sctpListener(t, "sctp6", &sctp.SCTPAddr{IPAddrs: []net.IPAddr{{IP: net.ParseIP("::1")}}, Port: 0})
backendAddr := backend.LocalAddr().(*sctp.SCTPAddr)
config := ProxyConfig{
Proto: "sctp",
HostIP: frontendAddr.IPAddrs[0].IP,
HostPort: frontendAddr.Port,
ContainerIP: backendAddr.IPAddrs[0].IP,
ContainerPort: backendAddr.Port,
ListenSock: listener,
}
proxy, err := newProxy(config)
assert.NilError(t, err)
testProxyAt(t, "sctp", proxy, fmt.Sprintf("[%s]:%d", config.HostIP, config.HostPort), false)
}