diff --git a/internal/api/v1/docs/swagger.json b/internal/api/v1/docs/swagger.json index aad503bf..03ea87f4 100644 --- a/internal/api/v1/docs/swagger.json +++ b/internal/api/v1/docs/swagger.json @@ -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": [ diff --git a/internal/api/v1/docs/swagger.yaml b/internal/api/v1/docs/swagger.yaml index e4dd69b5..38cb6d7c 100644 --- a/internal/api/v1/docs/swagger.yaml +++ b/internal/api/v1/docs/swagger.yaml @@ -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' diff --git a/internal/route/route.go b/internal/route/route.go index ef5cec60..f1f162db 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -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)) } diff --git a/internal/route/stream.go b/internal/route/stream.go index 0c38ef74..7c97bef2 100755 --- a/internal/route/stream.go +++ b/internal/route/stream.go @@ -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) } diff --git a/internal/route/stream/tcp_tcp.go b/internal/route/stream/tcp_tcp.go index 1033d519..429c7cc4 100644 --- a/internal/route/stream/tcp_tcp.go +++ b/internal/route/stream/tcp_tcp.go @@ -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 diff --git a/internal/route/stream/udp_udp.go b/internal/route/stream/udp_udp.go index 20b813a9..8600e962 100644 --- a/internal/route/stream/udp_udp.go +++ b/internal/route/stream/udp_udp.go @@ -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()) }