2b1e4ca3a7
Covering nil-input safety, plugin backends (HTTP/gRPC/default), loaders (File/HTTP/Redis), selector strategies, edge cases (empty/invalid inputs), and hop-to-node metadata inheritance. All tests passing.
462 lines
11 KiB
Go
462 lines
11 KiB
Go
package node
|
|
|
|
import (
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/go-gost/core/logger"
|
|
"github.com/go-gost/x/config"
|
|
"github.com/go-gost/x/config/parsing"
|
|
xlogger "github.com/go-gost/x/logger"
|
|
|
|
// Register connector and dialer implementations needed for node parsing.
|
|
_ "github.com/go-gost/x/connector/http"
|
|
_ "github.com/go-gost/x/dialer/tcp"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
logger.SetDefault(xlogger.NewLogger(xlogger.OutputOption(io.Discard)))
|
|
m.Run()
|
|
}
|
|
|
|
func testLogger() logger.Logger {
|
|
return xlogger.NewLogger(xlogger.OutputOption(io.Discard))
|
|
}
|
|
|
|
func TestParseNode_Nil(t *testing.T) {
|
|
n, err := ParseNode("test-hop", nil, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n != nil {
|
|
t.Fatal("expected nil for nil config")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_Defaults(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "node1",
|
|
Addr: "example.com:8080",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_DefaultsEmptyName(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "",
|
|
Addr: "example.com:8080",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_DefaultsEmptyAddr(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "no-addr",
|
|
Addr: "",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_ExplicitConnector(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "explicit-conn",
|
|
Addr: "example.com:8080",
|
|
Connector: &config.ConnectorConfig{
|
|
Type: "http",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_UnregisteredConnector(t *testing.T) {
|
|
_, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "bad-conn",
|
|
Addr: "example.com:8080",
|
|
Connector: &config.ConnectorConfig{
|
|
Type: "nonexistent-connector-type",
|
|
},
|
|
}, testLogger())
|
|
if err == nil {
|
|
t.Fatal("expected error for unregistered connector type")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_UnregisteredDialer(t *testing.T) {
|
|
_, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "bad-dial",
|
|
Addr: "example.com:8080",
|
|
Dialer: &config.DialerConfig{
|
|
Type: "nonexistent-dialer-type",
|
|
},
|
|
}, testLogger())
|
|
if err == nil {
|
|
t.Fatal("expected error for unregistered dialer type")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_ExplicitDialer(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "explicit-dial",
|
|
Addr: "example.com:8080",
|
|
Dialer: &config.DialerConfig{
|
|
Type: "tcp",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithBypass(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "bypass-node",
|
|
Addr: "example.com:8080",
|
|
Bypass: "bp1",
|
|
Bypasses: []string{"bp2", "bp3"},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithResolver(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "resolver-node",
|
|
Addr: "example.com:8080",
|
|
Resolver: "my-resolver",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithHosts(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "hosts-node",
|
|
Addr: "example.com:8080",
|
|
Hosts: "my-hosts",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithNetwork(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "network-node",
|
|
Addr: "example.com:8080",
|
|
Network: "tcp",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithMetadata(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "meta-node",
|
|
Addr: "example.com:8080",
|
|
Metadata: map[string]any{
|
|
parsing.MDKeySoMark: 1234,
|
|
parsing.MDKeyInterface: "eth0",
|
|
parsing.MDKeyNetns: "myns",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithSockOpts(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "sockopts-node",
|
|
Addr: "example.com:8080",
|
|
SockOpts: &config.SockOptsConfig{
|
|
Mark: 9999,
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithFilter(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "filter-node",
|
|
Addr: "example.com:8080",
|
|
Filter: &config.NodeFilterConfig{
|
|
Protocol: "http",
|
|
Host: "*.example.com",
|
|
Path: "/api",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithFilterStarBare(t *testing.T) {
|
|
// "*example.com" should become ".example.com"
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "star-bare-node",
|
|
Addr: "example.com:8080",
|
|
Filter: &config.NodeFilterConfig{
|
|
Host: "*example.com",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithMatcher(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "matcher-node",
|
|
Addr: "example.com:8080",
|
|
Matcher: &config.NodeMatcherConfig{
|
|
Rule: "dstport==80",
|
|
Priority: 10,
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithMatcherAutoPriority(t *testing.T) {
|
|
// Priority auto-set to len(rule) when zero
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "auto-prio-node",
|
|
Addr: "example.com:8080",
|
|
Matcher: &config.NodeMatcherConfig{
|
|
Rule: "dstport==443",
|
|
Priority: 0,
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithHTTPNode(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "http-node",
|
|
Addr: "example.com:8080",
|
|
HTTP: &config.HTTPNodeConfig{
|
|
Host: "custom.example.com",
|
|
RequestHeader: map[string]string{"X-Custom": "value"},
|
|
ResponseHeader: map[string]string{"X-Response": "value"},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithHTTPAuth(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "http-auth-node",
|
|
Addr: "example.com:8080",
|
|
HTTP: &config.HTTPNodeConfig{
|
|
Auth: &config.AuthConfig{
|
|
Username: "user",
|
|
Password: "pass",
|
|
},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithHTTPDeprecatedHeader(t *testing.T) {
|
|
// Deprecated Header field should populate RequestHeader
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "http-deprecated-node",
|
|
Addr: "example.com:8080",
|
|
HTTP: &config.HTTPNodeConfig{
|
|
Header: map[string]string{"X-Old": "value"},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_WithTLSNode(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "tls-node",
|
|
Addr: "example.com:8080",
|
|
TLS: &config.TLSNodeConfig{
|
|
ServerName: "secure.example.com",
|
|
Secure: true,
|
|
Options: &config.TLSOptions{
|
|
MinVersion: "1.2",
|
|
MaxVersion: "1.3",
|
|
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
|
|
ALPN: []string{"h2", "http/1.1"},
|
|
},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_TLSServerNameFromAddr(t *testing.T) {
|
|
// When connector/dialer TLS ServerName is empty, it defaults from addr hostname.
|
|
// The server name is set on the connector's TLS config (internal), so the best
|
|
// verification is that ParseNode does not error and returns a node.
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "tls-servername-node",
|
|
Addr: "tls.example.com:443",
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_ConnectorAuth(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "conn-auth-node",
|
|
Addr: "example.com:8080",
|
|
Connector: &config.ConnectorConfig{
|
|
Type: "http",
|
|
Auth: &config.AuthConfig{
|
|
Username: "connuser",
|
|
Password: "connpass",
|
|
},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_DialerAuth(t *testing.T) {
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "dial-auth-node",
|
|
Addr: "example.com:8080",
|
|
Dialer: &config.DialerConfig{
|
|
Type: "tcp",
|
|
Auth: &config.AuthConfig{
|
|
Username: "dialuser",
|
|
Password: "dialpass",
|
|
},
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_InvalidMatcherRule(t *testing.T) {
|
|
// An invalid matcher rule sets priority to -1
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "invalid-matcher-node",
|
|
Addr: "example.com:8080",
|
|
Matcher: &config.NodeMatcherConfig{
|
|
Rule: "",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node even with empty matcher rule")
|
|
}
|
|
}
|
|
|
|
func TestParseNode_DeprecatedFilterFields(t *testing.T) {
|
|
// Deprecated filter fields: Protocol/Host/Path via Filter
|
|
n, err := ParseNode("test-hop", &config.NodeConfig{
|
|
Name: "deprecated-node",
|
|
Addr: "example.com:8080",
|
|
Filter: &config.NodeFilterConfig{
|
|
Protocol: "socks5",
|
|
Host: "*.example.com",
|
|
Path: "/proxy",
|
|
},
|
|
}, testLogger())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if n == nil {
|
|
t.Fatal("expected non-nil node")
|
|
}
|
|
}
|