fix(tunnel, forward): route tunneled hostname through hop filter.host

The tunnel bind conns (bindConn/bindUDPConn) now expose their metadata (the
tunneled hostname) via the xctx.Context interface, so the service accept loop
forwards it into the handler context. handleRawForwarding selects a hop node by
that hostname when a hop is configured, so a node with a matching filter.host is
picked; otherwise it falls back to dialing the hostname directly. Fixes
reverse-proxy-tunnel TCP routing (gost#898) where the docs example failed with
"node not available" because the mux path never set the host metadata.
This commit is contained in:
ginuerzh
2026-08-26 20:28:37 +08:00
parent 6ad7269644
commit 9292e0af3e
4 changed files with 116 additions and 6 deletions
+16
View File
@@ -1,6 +1,7 @@
package tunnel
import (
"context"
"encoding/binary"
"errors"
"fmt"
@@ -11,6 +12,7 @@ import (
"github.com/go-gost/core/common/bufpool"
mdata "github.com/go-gost/core/metadata"
"github.com/go-gost/relay"
ictx "github.com/go-gost/x/internal/ctx"
xrelay "github.com/go-gost/x/internal/util/relay"
)
@@ -94,6 +96,14 @@ func (c *bindConn) Metadata() mdata.Metadata {
return c.md
}
// Context implements xctx.Context, exposing the connection metadata (e.g. the
// tunneled hostname) to the handler's context. The service accept loop derives
// the handler context from a conn that implements this interface, which lets
// reverse-forward handlers (rtcp) route on the hostname carried by the tunnel.
func (c *bindConn) Context() context.Context {
return ictx.ContextWithMetadata(context.Background(), c.md)
}
type bindUDPConn struct {
net.Conn
localAddr net.Addr
@@ -162,6 +172,12 @@ func (c *bindUDPConn) Metadata() mdata.Metadata {
return c.md
}
// Context implements xctx.Context, exposing the connection metadata (e.g. the
// tunneled hostname) to the handler's context. See bindConn.Context.
func (c *bindUDPConn) Context() context.Context {
return ictx.ContextWithMetadata(context.Background(), c.md)
}
type bindAddr struct {
network string
addr string
+45
View File
@@ -0,0 +1,45 @@
package tunnel
import (
"net"
"testing"
mdata "github.com/go-gost/core/metadata"
ictx "github.com/go-gost/x/internal/ctx"
xmd "github.com/go-gost/x/metadata"
mdutil "github.com/go-gost/x/metadata/util"
)
// TestBindConnContext verifies that a bindConn exposes its connection metadata
// (the tunneled hostname) via the xctx.Context interface, so the service accept
// loop forwards it into the handler context for filter.host routing (gost#898).
func TestBindConnContext(t *testing.T) {
md := mdata.Metadata(xmd.NewMetadata(map[string]any{"host": "example.local:80"}))
cn := &bindConn{
Conn: &net.TCPConn{},
localAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0},
remoteAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 80},
md: md,
}
got := mdutil.GetString(ictx.MetadataFromContext(cn.Context()), "host")
if got != "example.local:80" {
t.Errorf("host from Context() = %q, want %q", got, "example.local:80")
}
}
// TestBindUDPConnContext is the same check for the UDP reverse-tunnel conn.
func TestBindUDPConnContext(t *testing.T) {
md := mdata.Metadata(xmd.NewMetadata(map[string]any{"host": "example.local:80"}))
cn := &bindUDPConn{
Conn: &net.TCPConn{},
localAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0},
remoteAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 80},
md: md,
}
got := mdutil.GetString(ictx.MetadataFromContext(cn.Context()), "host")
if got != "example.local:80" {
t.Errorf("host from Context() = %q, want %q", got, "example.local:80")
}
}
+9
View File
@@ -93,6 +93,15 @@ func (h *forwardHandler) handleRawForwarding(ctx context.Context, conn net.Conn,
// metadata when set, then falling back to hop selection.
func (h *forwardHandler) selectTarget(ctx context.Context, proto string) *chain.Node {
if host := mdutil.GetString(ictx.MetadataFromContext(ctx), "host"); host != "" {
// Route the tunneled hostname through the hop so a node with a matching
// filter.host is selected (the node's addr is the real target). If the
// hop has no matching node (e.g. no filter configured), fall back to
// dialing the hostname directly — the pre-regression behavior.
if curHop := h.getHop(); curHop != nil {
if node := curHop.Select(ctx, hop.ProtocolSelectOption(proto), hop.HostSelectOption(host)); node != nil {
return node
}
}
return &chain.Node{Addr: host}
}
if curHop := h.getHop(); curHop != nil {
+46 -6
View File
@@ -201,7 +201,52 @@ func TestHandleRawForwarding_MarkerReset(t *testing.T) {
}
}
func TestHandleRawForwarding_ContextHost(t *testing.T) {
// TestHandleRawForwarding_ContextHostHop: a tunneled hostname with a hop whose
// node matches the host filter routes to that node's addr (the documented
// filter.host client routing). The node addr is the real target.
func TestHandleRawForwarding_ContextHostHop(t *testing.T) {
server, client := net.Pipe()
defer server.Close()
defer client.Close()
var gotHost string
h := newInitdHandler(withRouter(&mockRouter{
opts: &chain.RouterOptions{},
dialFn: func(ctx context.Context, network, address string) (net.Conn, error) {
return server, nil
},
}))
h.Forward(&mockHop{
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
var so hop.SelectOptions
for _, o := range opts {
o(&so)
}
gotHost = so.Host
return &chain.Node{Name: "example", Addr: "10.0.0.2:80"}
},
})
ctx := ictx.ContextWithMetadata(context.Background(), xmd.NewMetadata(map[string]any{
"host": "example.local:80",
}))
conn := newStringConn(nil)
ro := &xrecorder.HandlerRecorderObject{}
err := h.handleRawForwarding(ctx, conn, ro, nopLog(), "tcp", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gotHost != "example.local:80" {
t.Errorf("expected host passed to hop selection, got '%s'", gotHost)
}
if ro.Host != "10.0.0.2:80" {
t.Errorf("expected node addr to be dialed, got '%s'", ro.Host)
}
}
// TestHandleRawForwarding_ContextHostNoHop: when there is no hop, the tunneled
// hostname is dialed directly (fallback preserving the pre-regression behavior).
func TestHandleRawForwarding_ContextHostNoHop(t *testing.T) {
server, client := net.Pipe()
defer server.Close()
defer client.Close()
@@ -212,11 +257,6 @@ func TestHandleRawForwarding_ContextHost(t *testing.T) {
return server, nil
},
}))
h.Forward(&mockHop{
selectFn: func(ctx context.Context, opts ...hop.SelectOption) *chain.Node {
return &chain.Node{Addr: "10.0.0.2:80"}
},
})
ctx := ictx.ContextWithMetadata(context.Background(), xmd.NewMetadata(map[string]any{
"host": "10.0.0.99:8080",
}))