From 99a0f7323d49acc580f0c7a8dfd7a1aa91437102 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Mon, 22 Jun 2026 21:42:00 +0800 Subject: [PATCH] feat(listener): add SO_REUSEPORT support for TCP listeners (#654) Add opt-in reuseport metadata option for tcp, mtcp, and redirect/tcp listeners, allowing a GOST listener on a specific IP to coexist with another process on the same port but different address (e.g., Nginx on 0.0.0.0:443 + GOST on 127.0.0.1:443). With SO_REUSEPORT, the kernel routes to the most specific address match. On Unix, sets SO_REUSEPORT via ListenConfig.Control before bind(). On Windows, the option is a no-op (SO_REUSEADDR is the default). Closes go-gost/gost#654 --- listener/mtcp/listener.go | 3 +++ listener/mtcp/listener_unix.go | 17 +++++++++++++++++ listener/mtcp/listener_windows.go | 11 +++++++++++ listener/mtcp/metadata.go | 6 ++++-- listener/redirect/tcp/listener.go | 2 +- listener/redirect/tcp/listener_darwin.go | 9 +++++++++ listener/redirect/tcp/listener_linux.go | 15 +++++++++++---- listener/redirect/tcp/listener_other.go | 16 ++++++++++++++-- listener/redirect/tcp/listener_windows.go | 14 ++++++++++++++ listener/redirect/tcp/metadata.go | 6 ++++-- listener/tcp/listener.go | 3 +++ listener/tcp/listener_unix.go | 17 +++++++++++++++++ listener/tcp/listener_windows.go | 11 +++++++++++ listener/tcp/metadata.go | 4 +++- 14 files changed, 122 insertions(+), 12 deletions(-) create mode 100644 listener/mtcp/listener_unix.go create mode 100644 listener/mtcp/listener_windows.go create mode 100644 listener/redirect/tcp/listener_windows.go create mode 100644 listener/tcp/listener_unix.go create mode 100644 listener/tcp/listener_windows.go diff --git a/listener/mtcp/listener.go b/listener/mtcp/listener.go index 2cf29557..17eb7be5 100644 --- a/listener/mtcp/listener.go +++ b/listener/mtcp/listener.go @@ -59,6 +59,9 @@ func (l *mtcpListener) Init(md md.Metadata) (err error) { lc.SetMultipathTCP(true) l.logger.Debugf("mptcp enabled: %v", lc.MultipathTCP()) } + if l.md.reuseport { + lc.Control = l.setReusePort + } ln, err := lc.Listen(context.Background(), network, l.options.Addr) if err != nil { return diff --git a/listener/mtcp/listener_unix.go b/listener/mtcp/listener_unix.go new file mode 100644 index 00000000..0dc18ce5 --- /dev/null +++ b/listener/mtcp/listener_unix.go @@ -0,0 +1,17 @@ +//go:build !windows + +package mtcp + +import ( + "syscall" + + "golang.org/x/sys/unix" +) + +func (l *mtcpListener) setReusePort(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { + l.logger.Errorf("failed to set SO_REUSEPORT: %v", err) + } + }) +} diff --git a/listener/mtcp/listener_windows.go b/listener/mtcp/listener_windows.go new file mode 100644 index 00000000..2a457cc8 --- /dev/null +++ b/listener/mtcp/listener_windows.go @@ -0,0 +1,11 @@ +//go:build windows + +package mtcp + +import ( + "syscall" +) + +func (l *mtcpListener) setReusePort(network, address string, c syscall.RawConn) error { + return nil +} diff --git a/listener/mtcp/metadata.go b/listener/mtcp/metadata.go index 82004b65..be544360 100644 --- a/listener/mtcp/metadata.go +++ b/listener/mtcp/metadata.go @@ -13,8 +13,9 @@ const ( ) type metadata struct { - mptcp bool - muxCfg *mux.Config + mptcp bool + reuseport bool + muxCfg *mux.Config backlog int keepalive bool keepaliveIdle time.Duration @@ -24,6 +25,7 @@ type metadata struct { func (l *mtcpListener) parseMetadata(md md.Metadata) (err error) { l.md.mptcp = mdutil.GetBool(md, "mptcp") + l.md.reuseport = mdutil.GetBool(md, "reuseport") l.md.muxCfg = &mux.Config{ Version: mdutil.GetInt(md, "mux.version"), diff --git a/listener/redirect/tcp/listener.go b/listener/redirect/tcp/listener.go index 307a0328..7c7bad06 100644 --- a/listener/redirect/tcp/listener.go +++ b/listener/redirect/tcp/listener.go @@ -53,7 +53,7 @@ func (l *redirectListener) Init(md md.Metadata) (err error) { network = "tcp4" } lc := net.ListenConfig{} - if l.md.tproxy { + if l.md.tproxy || l.md.reuseport { lc.Control = l.control } if l.md.mptcp { diff --git a/listener/redirect/tcp/listener_darwin.go b/listener/redirect/tcp/listener_darwin.go index d42f002a..d483d986 100644 --- a/listener/redirect/tcp/listener_darwin.go +++ b/listener/redirect/tcp/listener_darwin.go @@ -2,8 +2,17 @@ package tcp import ( "syscall" + + "golang.org/x/sys/unix" ) func (l *redirectListener) control(network, address string, c syscall.RawConn) error { + if l.md.reuseport { + return c.Control(func(fd uintptr) { + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { + l.logger.Errorf("failed to set SO_REUSEPORT: %v", err) + } + }) + } return nil } diff --git a/listener/redirect/tcp/listener_linux.go b/listener/redirect/tcp/listener_linux.go index 581f7381..b6a2e710 100644 --- a/listener/redirect/tcp/listener_linux.go +++ b/listener/redirect/tcp/listener_linux.go @@ -8,11 +8,18 @@ import ( func (l *redirectListener) control(network, address string, c syscall.RawConn) error { return c.Control(func(fd uintptr) { - if err := unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_TRANSPARENT, 1); err != nil { - l.logger.Errorf("SetsockoptInt(SOL_IP, IP_TRANSPARENT, 1): %v", err) + if l.md.tproxy { + if err := unix.SetsockoptInt(int(fd), unix.SOL_IP, unix.IP_TRANSPARENT, 1); err != nil { + l.logger.Errorf("failed to set IP_TRANSPARENT: %v", err) + } + if err := unix.SetsockoptInt(int(fd), unix.SOL_IPV6, unix.IPV6_TRANSPARENT, 1); err != nil { + l.logger.Errorf("failed to set IPV6_TRANSPARENT: %v", err) + } } - if err := unix.SetsockoptInt(int(fd), unix.SOL_IPV6, unix.IPV6_TRANSPARENT, 1); err != nil { - l.logger.Errorf("SetsockoptInt(SOL_IPV6, IPV6_TRANSPARENT, 1): %v", err) + if l.md.reuseport { + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { + l.logger.Errorf("failed to set SO_REUSEPORT: %v", err) + } } }) } diff --git a/listener/redirect/tcp/listener_other.go b/listener/redirect/tcp/listener_other.go index 74471136..4d259f42 100644 --- a/listener/redirect/tcp/listener_other.go +++ b/listener/redirect/tcp/listener_other.go @@ -1,12 +1,24 @@ -//go:build !linux && !darwin +//go:build !linux && !darwin && !windows package tcp import ( "errors" "syscall" + + "golang.org/x/sys/unix" ) func (l *redirectListener) control(network, address string, c syscall.RawConn) error { - return errors.New("TProxy is not available on non-linux platform") + if l.md.tproxy { + return errors.New("TProxy is not available on non-linux platform") + } + if l.md.reuseport { + return c.Control(func(fd uintptr) { + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { + l.logger.Errorf("failed to set SO_REUSEPORT: %v", err) + } + }) + } + return nil } diff --git a/listener/redirect/tcp/listener_windows.go b/listener/redirect/tcp/listener_windows.go new file mode 100644 index 00000000..4b2af4af --- /dev/null +++ b/listener/redirect/tcp/listener_windows.go @@ -0,0 +1,14 @@ +package tcp + +import ( + "errors" + "syscall" +) + +func (l *redirectListener) control(network, address string, c syscall.RawConn) error { + if l.md.tproxy { + return errors.New("TProxy is not available on non-linux platform") + } + // SO_REUSEADDR is the default on Windows, so reuseport is a no-op. + return nil +} diff --git a/listener/redirect/tcp/metadata.go b/listener/redirect/tcp/metadata.go index b10d6971..2f09ba16 100644 --- a/listener/redirect/tcp/metadata.go +++ b/listener/redirect/tcp/metadata.go @@ -6,13 +6,15 @@ import ( ) type metadata struct { - tproxy bool - mptcp bool + tproxy bool + mptcp bool + reuseport bool } func (l *redirectListener) parseMetadata(md mdata.Metadata) (err error) { l.md.tproxy = mdutil.GetBool(md, "tproxy") l.md.mptcp = mdutil.GetBool(md, "mptcp") + l.md.reuseport = mdutil.GetBool(md, "reuseport") return } diff --git a/listener/tcp/listener.go b/listener/tcp/listener.go index 77105657..1dfb2c67 100644 --- a/listener/tcp/listener.go +++ b/listener/tcp/listener.go @@ -56,6 +56,9 @@ func (l *tcpListener) Init(md md.Metadata) (err error) { lc.SetMultipathTCP(true) l.logger.Debugf("mptcp enabled: %v", lc.MultipathTCP()) } + if l.md.reuseport { + lc.Control = l.setReusePort + } ln, err := lc.Listen(context.Background(), network, l.options.Addr) if err != nil { return diff --git a/listener/tcp/listener_unix.go b/listener/tcp/listener_unix.go new file mode 100644 index 00000000..5245fcec --- /dev/null +++ b/listener/tcp/listener_unix.go @@ -0,0 +1,17 @@ +//go:build !windows + +package tcp + +import ( + "syscall" + + "golang.org/x/sys/unix" +) + +func (l *tcpListener) setReusePort(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil { + l.logger.Errorf("failed to set SO_REUSEPORT: %v", err) + } + }) +} diff --git a/listener/tcp/listener_windows.go b/listener/tcp/listener_windows.go new file mode 100644 index 00000000..13c11549 --- /dev/null +++ b/listener/tcp/listener_windows.go @@ -0,0 +1,11 @@ +//go:build windows + +package tcp + +import ( + "syscall" +) + +func (l *tcpListener) setReusePort(network, address string, c syscall.RawConn) error { + return nil +} diff --git a/listener/tcp/metadata.go b/listener/tcp/metadata.go index cf45f327..081bf9ea 100644 --- a/listener/tcp/metadata.go +++ b/listener/tcp/metadata.go @@ -8,7 +8,8 @@ import ( ) type metadata struct { - mptcp bool + mptcp bool + reuseport bool keepalive bool keepaliveIdle time.Duration @@ -18,6 +19,7 @@ type metadata struct { func (l *tcpListener) parseMetadata(md md.Metadata) (err error) { l.md.mptcp = mdutil.GetBool(md, "mptcp") + l.md.reuseport = mdutil.GetBool(md, "reuseport") l.md.keepalive = mdutil.GetBool(md, "keepalive") l.md.keepaliveIdle = mdutil.GetDuration(md, "keepalive.idle")