feat(route): add bind address support for TCP/UDP routes

- Introduced a new `Bind` field in the route configuration to specify the address to listen on for TCP and UDP routes.
- Defaulted the bind address to "0.0.0.0" if not provided.
- Enhanced validation to ensure the bind address is a valid IP.
- Updated stream initialization to use the correct network type (tcp4/tcp6 or udp4/udp6) based on the bind address.
- Refactored stream creation functions to accept the network type as a parameter.
This commit is contained in:
yusing
2026-01-07 15:05:55 +08:00
parent 9205af3a4f
commit 25ceb512b4
6 changed files with 66 additions and 21 deletions

View File

@@ -4189,6 +4189,11 @@
"x-nullable": false,
"x-omitempty": false
},
"bind": {
"description": "for TCP and UDP routes, bind address to listen on",
"type": "string",
"x-nullable": true
},
"container": {
"description": "Docker only",
"allOf": [
@@ -5327,6 +5332,11 @@
"x-nullable": false,
"x-omitempty": false
},
"bind": {
"description": "for TCP and UDP routes, bind address to listen on",
"type": "string",
"x-nullable": true
},
"container": {
"description": "Docker only",
"allOf": [

View File

@@ -879,6 +879,10 @@ definitions:
type: string
alias:
type: string
bind:
description: for TCP and UDP routes, bind address to listen on
type: string
x-nullable: true
container:
allOf:
- $ref: '#/definitions/Container'
@@ -1495,6 +1499,10 @@ definitions:
type: string
alias:
type: string
bind:
description: for TCP and UDP routes, bind address to listen on
type: string
x-nullable: true
container:
allOf:
- $ref: '#/definitions/Container'

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"net"
"net/url"
"os"
"reflect"
@@ -47,6 +48,9 @@ type (
Host string `json:"host,omitempty"`
Port route.Port `json:"port"`
// for TCP and UDP routes, bind address to listen on
Bind string `json:"bind,omitempty" validate:"omitempty,ip_addr" extensions:"x-nullable"`
Root string `json:"root,omitempty"`
SPA bool `json:"spa,omitempty"` // Single-page app mode: serves index for non-existent paths
Index string `json:"index,omitempty"` // Index file to serve for single-page app mode
@@ -278,7 +282,28 @@ func (r *Route) validate() gperr.Error {
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s:%d", r.Scheme, r.Host, r.Port.Proxy))
case route.SchemeTCP, route.SchemeUDP:
if !r.ShouldExclude() {
r.LisURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://:%d", r.Scheme, r.Port.Listening))
if r.Bind == "" {
r.Bind = "0.0.0.0"
}
bindIP := net.ParseIP(r.Bind)
if bindIP == nil {
return gperr.Errorf("invalid bind address %s", r.Bind)
}
var scheme string
if bindIP.To4() == nil { // IPv6
if r.Scheme == route.SchemeTCP {
scheme = "tcp6"
} else {
scheme = "udp6"
}
} else {
if r.Scheme == route.SchemeTCP {
scheme = "tcp4"
} else {
scheme = "udp4"
}
}
r.LisURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s:%d", scheme, r.Bind, r.Port.Listening))
}
r.ProxyURL = gperr.Collect(&errs, nettypes.ParseURL, fmt.Sprintf("%s://%s:%d", r.Scheme, r.Host, r.Port.Proxy))
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net"
"strings"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@@ -30,7 +31,7 @@ func NewStreamRoute(base *Route) (types.Route, gperr.Error) {
return &StreamRoute{
Route: base,
l: log.With().
Str("type", string(base.Scheme)).
Str("type", base.LisURL.Scheme).
Str("name", base.Name()).
Logger(),
}, nil
@@ -99,7 +100,9 @@ func (r *StreamRoute) LocalAddr() net.Addr {
func (r *StreamRoute) initStream() (nettypes.Stream, error) {
lurl, rurl := r.LisURL, r.ProxyURL
if lurl != nil && lurl.Scheme != rurl.Scheme {
// lurl scheme is either tcp4/tcp6 -> tcp, udp4/udp6 -> udp
// rurl scheme does not have the trailing 4/6
if strings.TrimRight(lurl.Scheme, "46") != rurl.Scheme {
return nil, fmt.Errorf("incoherent scheme is not yet supported: %s != %s", lurl.Scheme, rurl.Scheme)
}
@@ -110,9 +113,9 @@ func (r *StreamRoute) initStream() (nettypes.Stream, error) {
switch rurl.Scheme {
case "tcp":
return stream.NewTCPTCPStream(laddr, rurl.Host)
return stream.NewTCPTCPStream(r.LisURL.Scheme, laddr, rurl.Host)
case "udp":
return stream.NewUDPUDPStream(laddr, rurl.Host)
return stream.NewUDPUDPStream(r.LisURL.Scheme, laddr, rurl.Host)
}
return nil, fmt.Errorf("unknown scheme: %s", rurl.Scheme)
}

View File

@@ -14,6 +14,7 @@ import (
)
type TCPTCPStream struct {
network string
listener net.Listener
laddr *net.TCPAddr
dst *net.TCPAddr
@@ -24,21 +25,21 @@ type TCPTCPStream struct {
closed atomic.Bool
}
func NewTCPTCPStream(listenAddr, dstAddr string) (nettypes.Stream, error) {
dst, err := net.ResolveTCPAddr("tcp", dstAddr)
func NewTCPTCPStream(network, listenAddr, dstAddr string) (nettypes.Stream, error) {
dst, err := net.ResolveTCPAddr(network, dstAddr)
if err != nil {
return nil, err
}
laddr, err := net.ResolveTCPAddr("tcp", listenAddr)
laddr, err := net.ResolveTCPAddr(network, listenAddr)
if err != nil {
return nil, err
}
return &TCPTCPStream{laddr: laddr, dst: dst}, nil
return &TCPTCPStream{network: network, laddr: laddr, dst: dst}, nil
}
func (s *TCPTCPStream) ListenAndServe(ctx context.Context, preDial, onRead nettypes.HookFunc) {
var err error
s.listener, err = net.ListenTCP("tcp", s.laddr)
s.listener, err = net.ListenTCP(s.network, s.laddr)
if err != nil {
logErr(s, err, "failed to listen")
return

View File

@@ -17,7 +17,7 @@ import (
)
type UDPUDPStream struct {
name string
network string
listener net.PacketConn
laddr *net.UDPAddr
@@ -51,25 +51,26 @@ const (
var bufPool = synk.GetSizedBytesPool()
func NewUDPUDPStream(listenAddr, dstAddr string) (nettypes.Stream, error) {
dst, err := net.ResolveUDPAddr("udp", dstAddr)
func NewUDPUDPStream(network, listenAddr, dstAddr string) (nettypes.Stream, error) {
dst, err := net.ResolveUDPAddr(network, dstAddr)
if err != nil {
return nil, err
}
laddr, err := net.ResolveUDPAddr("udp", listenAddr)
laddr, err := net.ResolveUDPAddr(network, listenAddr)
if err != nil {
return nil, err
}
return &UDPUDPStream{
laddr: laddr,
dst: dst,
conns: make(map[string]*udpUDPConn),
network: network,
laddr: laddr,
dst: dst,
conns: make(map[string]*udpUDPConn),
}, nil
}
func (s *UDPUDPStream) ListenAndServe(ctx context.Context, preDial, onRead nettypes.HookFunc) {
var err error
s.listener, err = net.ListenUDP("udp", s.laddr)
s.listener, err = net.ListenUDP(s.network, s.laddr)
if err != nil {
logErr(s, err, "failed to listen")
return
@@ -114,9 +115,6 @@ func (s *UDPUDPStream) LocalAddr() net.Addr {
func (s *UDPUDPStream) MarshalZerologObject(e *zerolog.Event) {
e.Str("protocol", "udp-udp")
if s.name != "" {
e.Str("name", s.name)
}
if s.dst != nil {
e.Str("dst", s.dst.String())
}