feat(listener/runix): support remote UDS forwarding (#102)

This commit is contained in:
Yuan Tong
2026-06-13 13:39:14 +08:00
committed by GitHub
parent 9f610fd163
commit 8b0806bad4
14 changed files with 247 additions and 34 deletions
+6
View File
@@ -86,6 +86,12 @@ func (*defaultRoute) Bind(ctx context.Context, network, address string, opts ...
Logger: logger, Logger: logger,
}) })
return ln, err return ln, err
case "unix":
addr, err := net.ResolveUnixAddr(network, address)
if err != nil {
return nil, err
}
return net.ListenUnix(network, addr)
default: default:
err := fmt.Errorf("network %s unsupported", network) err := fmt.Errorf("network %s unsupported", network)
return nil, err return nil, err
+5 -5
View File
@@ -209,7 +209,7 @@ func BuildConfigFromCmd(serviceList, nodeList []string) (*config.Config, error)
for i, service := range services { for i, service := range services {
service.Name = fmt.Sprintf("%sservice-%d", namePrefix, i) service.Name = fmt.Sprintf("%sservice-%d", namePrefix, i)
if chain != nil { if chain != nil {
if service.Listener.Type == "rtcp" || service.Listener.Type == "rudp" { if service.Listener.Type == "rtcp" || service.Listener.Type == "rudp" || service.Listener.Type == "runix" {
service.Listener.Chain = chain.Name service.Listener.Chain = chain.Name
} else { } else {
service.Handler.Chain = chain.Name service.Handler.Chain = chain.Name
@@ -397,8 +397,8 @@ func buildServiceConfig(url *url.URL) ([]*config.ServiceConfig, error) {
listener = schemes[1] listener = schemes[1]
} }
// For path-based protocols (unix socket, serial), use path as address // For path-based protocols (Unix socket listeners and serial), use path as address
isPathBasedProtocol := listener == "unix" || listener == "serial" isPathBasedProtocol := listener == "unix" || listener == "runix" || listener == "serial"
var addrs []string var addrs []string
if isPathBasedProtocol { if isPathBasedProtocol {
@@ -457,7 +457,7 @@ func buildServiceConfig(url *url.URL) ([]*config.ServiceConfig, error) {
if listener == "tcp" || listener == "udp" || if listener == "tcp" || listener == "udp" ||
listener == "rtcp" || listener == "rudp" || listener == "rtcp" || listener == "rudp" ||
listener == "tun" || listener == "tap" || listener == "tun" || listener == "tap" ||
listener == "dns" || listener == "unix" || listener == "dns" || listener == "unix" || listener == "runix" ||
listener == "serial" { listener == "serial" {
handler = listener handler = listener
} else { } else {
@@ -714,7 +714,7 @@ func buildNodeConfig(url *url.URL, m map[string]any) (*config.NodeConfig, error)
nodeMd[k] = v nodeMd[k] = v
} }
// For path-based protocols (unix socket, serial), use path as address // For path-based protocols (Unix socket dialers and serial), use path as address
var nodeAddr string var nodeAddr string
isPathBasedProtocol := dialer == "unix" || dialer == "serial" isPathBasedProtocol := dialer == "unix" || dialer == "serial"
if isPathBasedProtocol { if isPathBasedProtocol {
+15 -4
View File
@@ -28,7 +28,7 @@ func (c *relayConnector) Bind(ctx context.Context, conn net.Conn, network, addre
} }
switch network { switch network {
case "tcp", "tcp4", "tcp6": case "tcp", "tcp4", "tcp6", "unix":
return c.bindTCP(ctx, conn, network, address, log) return c.bindTCP(ctx, conn, network, address, log)
case "udp", "udp4", "udp6": case "udp", "udp4", "udp6":
return c.bindUDP(ctx, conn, network, address, &options, log) return c.bindUDP(ctx, conn, network, address, &options, log)
@@ -52,6 +52,7 @@ func (c *relayConnector) bindTCP(ctx context.Context, conn net.Conn, network, ad
} }
return &bindListener{ return &bindListener{
network: network,
addr: laddr, addr: laddr,
session: session, session: session,
logger: log, logger: log,
@@ -97,13 +98,17 @@ func (c *relayConnector) bind(conn net.Conn, cmd relay.CmdType, network, address
nid := relay.NetworkTCP nid := relay.NetworkTCP
if network == "udp" || network == "udp4" || network == "udp6" { if network == "udp" || network == "udp4" || network == "udp6" {
nid = relay.NetworkUDP nid = relay.NetworkUDP
} else if network == "unix" {
nid = relay.NetworkUnix
} }
req.Features = append(req.Features, &relay.NetworkFeature{ req.Features = append(req.Features, &relay.NetworkFeature{
Network: nid, Network: nid,
}) })
fa := &relay.AddrFeature{} fa := &relay.AddrFeature{}
if h, _, e := net.SplitHostPort(address); e == nil && h == "" { if network != "unix" {
address = net.JoinHostPort("0.0.0.0", address) if h, _, e := net.SplitHostPort(address); e == nil && h == "" {
address = net.JoinHostPort("0.0.0.0", address)
}
} }
fa.ParseFrom(address) fa.ParseFrom(address)
req.Features = append(req.Features, fa) req.Features = append(req.Features, fa)
@@ -125,7 +130,11 @@ func (c *relayConnector) bind(conn net.Conn, cmd relay.CmdType, network, address
for _, f := range resp.Features { for _, f := range resp.Features {
if f.Type() == relay.FeatureAddr { if f.Type() == relay.FeatureAddr {
if fa, ok := f.(*relay.AddrFeature); ok { if fa, ok := f.(*relay.AddrFeature); ok {
addr = net.JoinHostPort(fa.Host, strconv.Itoa(int(fa.Port))) if network == "unix" {
addr = fa.Host
} else {
addr = net.JoinHostPort(fa.Host, strconv.Itoa(int(fa.Port)))
}
} }
} }
} }
@@ -137,6 +146,8 @@ func (c *relayConnector) bind(conn net.Conn, cmd relay.CmdType, network, address
baddr, err = net.ResolveTCPAddr(network, addr) baddr, err = net.ResolveTCPAddr(network, addr)
case "udp", "udp4", "udp6": case "udp", "udp4", "udp6":
baddr, err = net.ResolveUDPAddr(network, addr) baddr, err = net.ResolveUDPAddr(network, addr)
case "unix":
baddr, err = net.ResolveUnixAddr(network, addr)
default: default:
err = fmt.Errorf("unknown network %s", network) err = fmt.Errorf("unknown network %s", network)
} }
+7 -1
View File
@@ -62,7 +62,13 @@ func (p *bindListener) getPeerConn(conn net.Conn) (net.Conn, error) {
} }
} }
raddr, err := net.ResolveTCPAddr("tcp", address) var raddr net.Addr
var err error
if p.network == "unix" {
raddr, err = net.ResolveUnixAddr("unix", address)
} else {
raddr, err = net.ResolveTCPAddr("tcp", address)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
+4 -2
View File
@@ -74,8 +74,10 @@ func (c *sshdConnector) Bind(ctx context.Context, conn net.Conn, network, addres
return nil, errors.New("ssh: invalid connection") return nil, errors.New("ssh: invalid connection")
} }
if host, port, _ := net.SplitHostPort(address); host == "" { if network != "unix" {
address = net.JoinHostPort("0.0.0.0", port) if host, port, err := net.SplitHostPort(address); err == nil && host == "" {
address = net.JoinHostPort("0.0.0.0", port)
}
} }
return cc.Client().Listen(network, address) return cc.Client().Listen(network, address)
+6 -6
View File
@@ -42,13 +42,13 @@ func (h *forwardHandler) dialTarget(ctx context.Context, conn net.Conn, ro *xrec
} }
addr := target.Addr addr := target.Addr
if opts := target.Options(); opts != nil { if opts := target.Options(); opts != nil {
switch opts.Network { if opts.Network != "" {
case "unix":
network = opts.Network network = opts.Network
default: }
if _, _, err := net.SplitHostPort(addr); err != nil { }
addr += ":0" if network != "unix" {
} if _, _, err := net.SplitHostPort(addr); err != nil {
addr += ":0"
} }
} }
+6 -6
View File
@@ -28,13 +28,13 @@ func (h *forwardHandler) handleRawForwarding(ctx context.Context, conn net.Conn,
} }
addr := target.Addr addr := target.Addr
if opts := target.Options(); opts != nil { if opts := target.Options(); opts != nil {
switch opts.Network { if opts.Network != "" {
case "unix":
network = opts.Network network = opts.Network
default: }
if _, _, err := net.SplitHostPort(addr); err != nil { }
addr += ":0" if network != "unix" {
} if _, _, err := net.SplitHostPort(addr); err != nil {
addr += ":0"
} }
} }
+4 -3
View File
@@ -1,8 +1,8 @@
// Package remote implements a reverse forwarding handler for connections // Package remote implements a reverse forwarding handler for connections
// received from remote GOST nodes. It listens for "rtcp" and "rudp" protocols // received from remote GOST nodes. It listens for "rtcp", "rudp", and "runix" protocols
// and forwards the accepted connections to the configured hop. // and forwards the accepted connections to the configured hop.
// //
// The handler is registered under the names "rtcp" and "rudp" via // The handler is registered under the names "rtcp", "rudp", and "runix" via
// NewHandler in init(). // NewHandler in init().
// //
// # Connection processing flow // # Connection processing flow
@@ -85,6 +85,7 @@ import (
func init() { func init() {
registry.HandlerRegistry().Register("rtcp", NewHandler) registry.HandlerRegistry().Register("rtcp", NewHandler)
registry.HandlerRegistry().Register("rudp", NewHandler) registry.HandlerRegistry().Register("rudp", NewHandler)
registry.HandlerRegistry().Register("runix", NewHandler)
} }
type forwardHandler struct { type forwardHandler struct {
@@ -98,7 +99,7 @@ type forwardHandler struct {
} }
// NewHandler creates a remote forwarding handler with the given options. // NewHandler creates a remote forwarding handler with the given options.
// The handler registers for "rtcp" and "rudp" protocols. // The handler registers for "rtcp", "rudp", and "runix" protocols.
func NewHandler(opts ...handler.Option) handler.Handler { func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.Options{} options := handler.Options{}
for _, opt := range opts { for _, opt := range opts {
+1 -1
View File
@@ -22,7 +22,7 @@ func (h *forwardHandler) newRecorderObject(ctx context.Context, conn net.Conn, s
Service: h.options.Service, Service: h.options.Service,
RemoteAddr: conn.RemoteAddr().String(), RemoteAddr: conn.RemoteAddr().String(),
LocalAddr: conn.LocalAddr().String(), LocalAddr: conn.LocalAddr().String(),
Network: "tcp", Network: conn.LocalAddr().Network(),
Time: start, Time: start,
SID: xctx.SidFromContext(ctx).String(), SID: xctx.SidFromContext(ctx).String(),
} }
+13
View File
@@ -82,6 +82,19 @@ func TestNewRecorderObject_UDP(t *testing.T) {
} }
} }
func TestNewRecorderObject_Unix(t *testing.T) {
h := newInitdHandler()
conn := newStringConn(nil)
conn.local = &net.UnixAddr{Name: "/tmp/local.sock", Net: "unix"}
conn.remote = &net.UnixAddr{Name: "/tmp/remote.sock", Net: "unix"}
ro := h.newRecorderObject(context.Background(), conn, time.Now())
if ro.Network != "unix" {
t.Errorf("expected network unix, got %s", ro.Network)
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// checkRateLimit // checkRateLimit
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+8 -3
View File
@@ -99,10 +99,15 @@ func (h *relayHandler) handleBind(ctx context.Context, conn net.Conn, network, a
return err return err
} }
if network == "tcp" { switch network {
case "tcp", "tcp4", "tcp6", "unix":
return h.bindTCP(ctx, conn, network, address, ro, log) return h.bindTCP(ctx, conn, network, address, ro, log)
} else { case "udp", "udp4", "udp6":
return h.bindUDP(ctx, conn, network, address, ro, log) return h.bindUDP(ctx, conn, network, address, ro, log)
default:
resp.Status = relay.StatusBadRequest
resp.WriteTo(conn)
return fmt.Errorf("network %s is unsupported", network)
} }
} }
@@ -149,7 +154,7 @@ func (h *relayHandler) bindTCP(ctx context.Context, conn net.Conn, network, addr
serviceName := fmt.Sprintf("%s-ep-%s", h.options.Service, ln.Addr()) serviceName := fmt.Sprintf("%s-ep-%s", h.options.Service, ln.Addr())
log = log.WithFields(map[string]any{ log = log.WithFields(map[string]any{
"service": serviceName, "service": serviceName,
"listener": "tcp", "listener": network,
"handler": "ep-tcp", "handler": "ep-tcp",
"bind": fmt.Sprintf("%s/%s", ln.Addr(), ln.Addr().Network()), "bind": fmt.Sprintf("%s/%s", ln.Addr(), ln.Addr().Network()),
"src": ln.Addr().String(), "src": ln.Addr().String(),
+10 -1
View File
@@ -236,6 +236,7 @@ func (h *relayHandler) Handle(ctx context.Context, conn net.Conn, opts ...handle
var user, pass string var user, pass string
var address string var address string
var addrFeature *relay.AddrFeature
var networkID relay.NetworkID var networkID relay.NetworkID
for _, f := range req.Features { for _, f := range req.Features {
switch f.Type() { switch f.Type() {
@@ -245,7 +246,7 @@ func (h *relayHandler) Handle(ctx context.Context, conn net.Conn, opts ...handle
} }
case relay.FeatureAddr: case relay.FeatureAddr:
if feature, _ := f.(*relay.AddrFeature); feature != nil { if feature, _ := f.(*relay.AddrFeature); feature != nil {
address = net.JoinHostPort(feature.Host, strconv.Itoa(int(feature.Port))) addrFeature = feature
} }
case relay.FeatureNetwork: case relay.FeatureNetwork:
if feature, _ := f.(*relay.NetworkFeature); feature != nil { if feature, _ := f.(*relay.NetworkFeature); feature != nil {
@@ -275,6 +276,14 @@ func (h *relayHandler) Handle(ctx context.Context, conn net.Conn, opts ...handle
if (req.Cmd & relay.FUDP) == relay.FUDP { if (req.Cmd & relay.FUDP) == relay.FUDP {
network = "udp" network = "udp"
} }
if addrFeature != nil {
switch network {
case "unix", "serial":
address = addrFeature.Host
default:
address = net.JoinHostPort(addrFeature.Host, strconv.Itoa(int(addrFeature.Port)))
}
}
ro.Network = network ro.Network = network
ro.Host = address ro.Host = address
log = log.WithFields(map[string]any{"network": network}) log = log.WithFields(map[string]any{"network": network})
+149
View File
@@ -0,0 +1,149 @@
package runix
import (
"context"
"net"
"sync"
"github.com/go-gost/core/limiter"
"github.com/go-gost/core/listener"
"github.com/go-gost/core/logger"
md "github.com/go-gost/core/metadata"
admission "github.com/go-gost/x/admission/wrapper"
climiter "github.com/go-gost/x/limiter/conn/wrapper"
limiter_wrapper "github.com/go-gost/x/limiter/traffic/wrapper"
metrics "github.com/go-gost/x/metrics/wrapper"
stats "github.com/go-gost/x/observer/stats/wrapper"
"github.com/go-gost/x/registry"
)
func init() {
registry.ListenerRegistry().Register("runix", NewListener)
}
type runixListener struct {
laddr net.Addr
ln net.Listener
logger logger.Logger
closed chan struct{}
md metadata
options listener.Options
mu sync.Mutex
}
func NewListener(opts ...listener.Option) listener.Listener {
options := listener.Options{}
for _, opt := range opts {
opt(&options)
}
return &runixListener{
closed: make(chan struct{}),
logger: options.Logger,
options: options,
}
}
func (l *runixListener) Init(md md.Metadata) (err error) {
if err = l.parseMetadata(md); err != nil {
return
}
if laddr, _ := net.ResolveUnixAddr("unix", l.options.Addr); laddr != nil {
l.laddr = laddr
}
if l.laddr == nil {
l.laddr = &bindAddr{addr: l.options.Addr}
}
return
}
func (l *runixListener) Accept() (conn net.Conn, err error) {
select {
case <-l.closed:
return nil, net.ErrClosed
default:
}
ln := l.getListener()
if ln == nil {
ln, err = l.options.Router.Bind(context.Background(), "unix", l.laddr.String())
if err != nil {
return nil, listener.NewBindError(err)
}
ln = metrics.WrapListener(l.options.Service, ln)
ln = stats.WrapListener(ln, l.options.Stats)
ln = admission.WrapListener(l.options.Service, l.options.Admission, ln)
ln = limiter_wrapper.WrapListener(l.options.Service, ln, l.options.TrafficLimiter)
ln = climiter.WrapListener(l.options.ConnLimiter, ln)
l.setListener(ln)
}
select {
case <-l.closed:
ln.Close()
return nil, net.ErrClosed
default:
}
conn, err = ln.Accept()
if err != nil {
ln.Close()
l.setListener(nil)
return nil, listener.NewAcceptError(err)
}
conn = limiter_wrapper.WrapConn(
conn,
l.options.TrafficLimiter,
conn.RemoteAddr().String(),
limiter.ScopeOption(limiter.ScopeConn),
limiter.ServiceOption(l.options.Service),
limiter.NetworkOption(conn.LocalAddr().Network()),
limiter.SrcOption(conn.RemoteAddr().String()),
)
return
}
func (l *runixListener) Addr() net.Addr {
return l.laddr
}
func (l *runixListener) Close() error {
select {
case <-l.closed:
default:
close(l.closed)
if ln := l.getListener(); ln != nil {
ln.Close()
}
}
return nil
}
func (l *runixListener) setListener(ln net.Listener) {
l.mu.Lock()
defer l.mu.Unlock()
l.ln = ln
}
func (l *runixListener) getListener() net.Listener {
l.mu.Lock()
defer l.mu.Unlock()
return l.ln
}
type bindAddr struct {
addr string
}
func (p *bindAddr) Network() string {
return "unix"
}
func (p *bindAddr) String() string {
return p.addr
}
+11
View File
@@ -0,0 +1,11 @@
package runix
import (
mdata "github.com/go-gost/core/metadata"
)
type metadata struct{}
func (l *runixListener) parseMetadata(md mdata.Metadata) (err error) {
return
}