fix(matcher,plugin): case-insensitive matching, port accumulation, nil guards, doc comments
matcher: fix IPv6 normalization via netip.ParseAddr, port-range overwrite by switching to []*PortRange map, case-insensitive domain/host matching, glob.MustCompile→Compile to avoid panic on invalid patterns. plugin: guard nil *Options in NewGRPCConn and NewHTTPClient; add HTTPClientTransport helper for idle-connection cleanup. net/*: add doc comments for all exported symbols (no code changes). Add 30 tests (matcher_test.go 22, plugin_test.go 8) covering all matcher types, nil safety, case insensitivity, IPv6 normalization, port ranges, and plugin option composition.
This commit is contained in:
+59
-29
@@ -1,3 +1,5 @@
|
|||||||
|
// Package matcher provides pattern matching utilities for IP addresses,
|
||||||
|
// CIDR blocks, domains, wildcard patterns, and address:port combinations.
|
||||||
package matcher
|
package matcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,8 +13,8 @@ import (
|
|||||||
"github.com/yl2chen/cidranger"
|
"github.com/yl2chen/cidranger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Matcher is a generic pattern matcher,
|
// Matcher is a generic pattern matcher that tests whether a given value
|
||||||
// it gives the match result of the given pattern for specific v.
|
// matches any of the pre-configured patterns.
|
||||||
type Matcher interface {
|
type Matcher interface {
|
||||||
Match(v string) bool
|
Match(v string) bool
|
||||||
}
|
}
|
||||||
@@ -21,7 +23,7 @@ type ipMatcher struct {
|
|||||||
ips map[string]struct{}
|
ips map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPMatcher creates a Matcher with a list of IP addresses.
|
// IPMatcher creates a Matcher for a list of IP addresses.
|
||||||
func IPMatcher(ips []net.IP) Matcher {
|
func IPMatcher(ips []net.IP) Matcher {
|
||||||
matcher := &ipMatcher{
|
matcher := &ipMatcher{
|
||||||
ips: make(map[string]struct{}),
|
ips: make(map[string]struct{}),
|
||||||
@@ -36,34 +38,41 @@ func (m *ipMatcher) Match(ip string) bool {
|
|||||||
if m == nil || len(m.ips) == 0 {
|
if m == nil || len(m.ips) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
_, ok := m.ips[ip]
|
addr, err := netip.ParseAddr(ip)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := m.ips[addr.String()]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
type addrMatcher struct {
|
type addrMatcher struct {
|
||||||
addrs map[string]*xnet.PortRange
|
addrs map[string][]*xnet.PortRange
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddrMatcher creates a Matcher with a list of HOST:PORT addresses.
|
// AddrMatcher creates a Matcher for a list of HOST:PORT addresses.
|
||||||
// the host can be an IP (e.g. 192.168.1.1) address, a plain domain such as 'example.com',
|
// The host can be an IP address (e.g. 192.168.1.1), a plain domain such as
|
||||||
// or a special pattern '.example.com' that matches 'example.com'
|
// "example.com", or a special pattern ".example.com" that matches
|
||||||
// and any subdomain 'abc.example.com', 'def.abc.example.com' etc.
|
// "example.com" and any subdomain "abc.example.com", "def.abc.example.com"
|
||||||
// The PORT can be a single port number or port range MIN-MAX(e.g. 0-65535).
|
// etc. The PORT can be a single port number or port range MIN-MAX
|
||||||
|
// (e.g. 0-65535).
|
||||||
func AddrMatcher(addrs []string) Matcher {
|
func AddrMatcher(addrs []string) Matcher {
|
||||||
matcher := &addrMatcher{
|
matcher := &addrMatcher{
|
||||||
addrs: make(map[string]*xnet.PortRange),
|
addrs: make(map[string][]*xnet.PortRange),
|
||||||
}
|
}
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
host, port, _ := net.SplitHostPort(addr)
|
host, port, _ := net.SplitHostPort(addr)
|
||||||
if host == "" {
|
if host == "" {
|
||||||
matcher.addrs[addr] = nil
|
host = strings.ToLower(addr)
|
||||||
|
matcher.addrs[host] = append(matcher.addrs[host], nil)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
host = strings.ToLower(host)
|
||||||
pr := &xnet.PortRange{}
|
pr := &xnet.PortRange{}
|
||||||
if err := pr.Parse(port); err != nil {
|
if err := pr.Parse(port); err != nil {
|
||||||
pr = nil
|
pr = nil
|
||||||
}
|
}
|
||||||
matcher.addrs[host] = pr
|
matcher.addrs[host] = append(matcher.addrs[host], pr)
|
||||||
}
|
}
|
||||||
return matcher
|
return matcher
|
||||||
}
|
}
|
||||||
@@ -76,24 +85,25 @@ func (m *addrMatcher) Match(addr string) bool {
|
|||||||
if host == "" {
|
if host == "" {
|
||||||
host = addr
|
host = addr
|
||||||
}
|
}
|
||||||
|
host = strings.ToLower(host)
|
||||||
port, _ := strconv.Atoi(sp)
|
port, _ := strconv.Atoi(sp)
|
||||||
|
|
||||||
if pr, ok := m.addrs[host]; ok {
|
if prs, ok := m.addrs[host]; ok {
|
||||||
if pr == nil || pr.Contains(port) {
|
if portRangeMatch(prs, port) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if pr, ok := m.addrs["."+host]; ok {
|
if prs, ok := m.addrs["."+host]; ok {
|
||||||
if pr == nil || pr.Contains(port) {
|
if portRangeMatch(prs, port) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if index := strings.IndexByte(host, '.'); index > 0 {
|
if index := strings.IndexByte(host, '.'); index > 0 {
|
||||||
if pr, ok := m.addrs[host[index:]]; ok {
|
if prs, ok := m.addrs[host[index:]]; ok {
|
||||||
if pr == nil || pr.Contains(port) {
|
if portRangeMatch(prs, port) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +116,17 @@ func (m *addrMatcher) Match(addr string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// portRangeMatch returns true if any PortRange in prs contains port.
|
||||||
|
// A nil entry means "any port" and always matches.
|
||||||
|
func portRangeMatch(prs []*xnet.PortRange, port int) bool {
|
||||||
|
for _, pr := range prs {
|
||||||
|
if pr == nil || pr.Contains(port) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type cidrMatcher struct {
|
type cidrMatcher struct {
|
||||||
ranger cidranger.Ranger
|
ranger cidranger.Ranger
|
||||||
}
|
}
|
||||||
@@ -134,16 +155,16 @@ type domainMatcher struct {
|
|||||||
domains map[string]struct{}
|
domains map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DomainMatcher creates a Matcher for a list of domains,
|
// DomainMatcher creates a Matcher for a list of domains.
|
||||||
// the domain should be a plain domain such as 'example.com',
|
// The domain should be a plain domain such as "example.com",
|
||||||
// or a special pattern '.example.com' that matches 'example.com'
|
// or a special pattern ".example.com" that matches "example.com"
|
||||||
// and any subdomain 'abc.example.com', 'def.abc.example.com' etc.
|
// and any subdomain "abc.example.com", "def.abc.example.com" etc.
|
||||||
func DomainMatcher(domains []string) Matcher {
|
func DomainMatcher(domains []string) Matcher {
|
||||||
matcher := &domainMatcher{
|
matcher := &domainMatcher{
|
||||||
domains: make(map[string]struct{}),
|
domains: make(map[string]struct{}),
|
||||||
}
|
}
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
matcher.domains[domain] = struct{}{}
|
matcher.domains[strings.ToLower(domain)] = struct{}{}
|
||||||
}
|
}
|
||||||
return matcher
|
return matcher
|
||||||
}
|
}
|
||||||
@@ -153,6 +174,8 @@ func (m *domainMatcher) Match(domain string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
domain = strings.ToLower(domain)
|
||||||
|
|
||||||
if _, ok := m.domains[domain]; ok {
|
if _, ok := m.domains[domain]; ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -183,8 +206,9 @@ type wildcardMatcher struct {
|
|||||||
patterns []wildcardMatcherPattern
|
patterns []wildcardMatcherPattern
|
||||||
}
|
}
|
||||||
|
|
||||||
// WildcardMatcher creates a Matcher for a specific wildcard domain pattern,
|
// WildcardMatcher creates a Matcher for wildcard domain patterns.
|
||||||
// the pattern can be a wildcard such as '*.exmaple.com', '*.example.com:80', or '*.example.com:0-65535'
|
// The pattern can be a wildcard such as "*.example.com",
|
||||||
|
// "*.example.com:80", or "*.example.com:0-65535".
|
||||||
func WildcardMatcher(patterns []string) Matcher {
|
func WildcardMatcher(patterns []string) Matcher {
|
||||||
matcher := &wildcardMatcher{}
|
matcher := &wildcardMatcher{}
|
||||||
for _, pattern := range patterns {
|
for _, pattern := range patterns {
|
||||||
@@ -192,13 +216,17 @@ func WildcardMatcher(patterns []string) Matcher {
|
|||||||
if host == "" {
|
if host == "" {
|
||||||
host = pattern
|
host = pattern
|
||||||
}
|
}
|
||||||
|
g, err := glob.Compile(strings.ToLower(host))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
pr := &xnet.PortRange{}
|
pr := &xnet.PortRange{}
|
||||||
if err := pr.Parse(port); err != nil {
|
if err := pr.Parse(port); err != nil {
|
||||||
pr = nil
|
pr = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
matcher.patterns = append(matcher.patterns, wildcardMatcherPattern{
|
matcher.patterns = append(matcher.patterns, wildcardMatcherPattern{
|
||||||
glob: glob.MustCompile(host),
|
glob: g,
|
||||||
pr: pr,
|
pr: pr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -215,6 +243,7 @@ func (m *wildcardMatcher) Match(addr string) bool {
|
|||||||
if host == "" {
|
if host == "" {
|
||||||
host = addr
|
host = addr
|
||||||
}
|
}
|
||||||
|
host = strings.ToLower(host)
|
||||||
port, _ := strconv.Atoi(sp)
|
port, _ := strconv.Atoi(sp)
|
||||||
for _, pattern := range m.patterns {
|
for _, pattern := range m.patterns {
|
||||||
if pattern.glob.Match(host) {
|
if pattern.glob.Match(host) {
|
||||||
@@ -231,6 +260,7 @@ type ipRangeMatcher struct {
|
|||||||
ranges []xnet.IPRange
|
ranges []xnet.IPRange
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPRangeMatcher creates a Matcher for a list of IP ranges (e.g. "192.168.1.1-192.168.1.255").
|
||||||
func IPRangeMatcher(ranges []xnet.IPRange) Matcher {
|
func IPRangeMatcher(ranges []xnet.IPRange) Matcher {
|
||||||
matcher := &ipRangeMatcher{
|
matcher := &ipRangeMatcher{
|
||||||
ranges: ranges,
|
ranges: ranges,
|
||||||
@@ -260,9 +290,9 @@ func (m *ipRangeMatcher) Match(addr string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type nopMatcher struct {
|
type nopMatcher struct{}
|
||||||
}
|
|
||||||
|
|
||||||
|
// NopMatcher creates a Matcher that never matches anything.
|
||||||
func NopMatcher() Matcher {
|
func NopMatcher() Matcher {
|
||||||
return &nopMatcher{}
|
return &nopMatcher{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,438 @@
|
|||||||
|
package matcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
xnet "github.com/go-gost/x/internal/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- IPMatcher ----
|
||||||
|
|
||||||
|
func TestIPMatcher_Match(t *testing.T) {
|
||||||
|
m := IPMatcher([]net.IP{
|
||||||
|
net.ParseIP("192.168.1.1"),
|
||||||
|
net.ParseIP("10.0.0.1"),
|
||||||
|
})
|
||||||
|
if !m.Match("192.168.1.1") {
|
||||||
|
t.Error("expected match for 192.168.1.1")
|
||||||
|
}
|
||||||
|
if !m.Match("10.0.0.1") {
|
||||||
|
t.Error("expected match for 10.0.0.1")
|
||||||
|
}
|
||||||
|
if m.Match("172.16.0.1") {
|
||||||
|
t.Error("expected no match for 172.16.0.1")
|
||||||
|
}
|
||||||
|
if m.Match("") {
|
||||||
|
t.Error("expected no match for empty string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIPMatcher_EmptyIPs(t *testing.T) {
|
||||||
|
m := IPMatcher(nil)
|
||||||
|
if m.Match("192.168.1.1") {
|
||||||
|
t.Error("expected no match when no IPs configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIPMatcher_NilMatcher(t *testing.T) {
|
||||||
|
var m *ipMatcher
|
||||||
|
if m.Match("192.168.1.1") {
|
||||||
|
t.Error("expected no match on nil matcher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIPMatcher_NormalizedIPv6(t *testing.T) {
|
||||||
|
// Stored in compressed form, lookup with expanded form.
|
||||||
|
m := IPMatcher([]net.IP{net.ParseIP("2001:db8::1")})
|
||||||
|
if !m.Match("2001:db8:0:0:0:0:0:1") {
|
||||||
|
t.Error("expected match for IPv6 expanded form")
|
||||||
|
}
|
||||||
|
if !m.Match("2001:db8::1") {
|
||||||
|
t.Error("expected match for IPv6 compressed form")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIPMatcher_InvalidInput(t *testing.T) {
|
||||||
|
m := IPMatcher([]net.IP{net.ParseIP("1.2.3.4")})
|
||||||
|
if m.Match("not-an-ip") {
|
||||||
|
t.Error("expected no match for invalid IP string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- AddrMatcher ----
|
||||||
|
|
||||||
|
func TestAddrMatcher_ExactMatch(t *testing.T) {
|
||||||
|
m := AddrMatcher([]string{"example.com:80", "192.168.1.1:443"})
|
||||||
|
if !m.Match("example.com:80") {
|
||||||
|
t.Error("expected match for example.com:80")
|
||||||
|
}
|
||||||
|
if !m.Match("192.168.1.1:443") {
|
||||||
|
t.Error("expected match for 192.168.1.1:443")
|
||||||
|
}
|
||||||
|
if m.Match("example.com:443") {
|
||||||
|
t.Error("expected no match for example.com:443")
|
||||||
|
}
|
||||||
|
if m.Match("other.com:80") {
|
||||||
|
t.Error("expected no match for other.com:80")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddrMatcher_PortRange(t *testing.T) {
|
||||||
|
m := AddrMatcher([]string{"example.com:8000-9000"})
|
||||||
|
if !m.Match("example.com:8000") {
|
||||||
|
t.Error("expected match for port 8000 in range 8000-9000")
|
||||||
|
}
|
||||||
|
if !m.Match("example.com:9000") {
|
||||||
|
t.Error("expected match for port 9000 in range 8000-9000")
|
||||||
|
}
|
||||||
|
if !m.Match("example.com:8500") {
|
||||||
|
t.Error("expected match for port 8500 in range 8000-9000")
|
||||||
|
}
|
||||||
|
if m.Match("example.com:7999") {
|
||||||
|
t.Error("expected no match for port 7999 outside 8000-9000")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddrMatcher_NoPort(t *testing.T) {
|
||||||
|
m := AddrMatcher([]string{"example.com"})
|
||||||
|
if !m.Match("example.com:80") {
|
||||||
|
t.Error("expected match for any port when no port configured")
|
||||||
|
}
|
||||||
|
if !m.Match("example.com:443") {
|
||||||
|
t.Error("expected match for any port when no port configured")
|
||||||
|
}
|
||||||
|
if !m.Match("example.com") {
|
||||||
|
t.Error("expected match when no port in input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddrMatcher_Subdomain(t *testing.T) {
|
||||||
|
m := AddrMatcher([]string{".example.com:80"})
|
||||||
|
if !m.Match("example.com:80") {
|
||||||
|
t.Error("expected match for example.com:80 with .example.com rule")
|
||||||
|
}
|
||||||
|
if !m.Match("sub.example.com:80") {
|
||||||
|
t.Error("expected match for sub.example.com:80 with .example.com rule")
|
||||||
|
}
|
||||||
|
if !m.Match("deep.sub.example.com:80") {
|
||||||
|
t.Error("expected match for deep.sub.example.com:80 with .example.com rule")
|
||||||
|
}
|
||||||
|
if m.Match("other.com:80") {
|
||||||
|
t.Error("expected no match for other.com:80")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddrMatcher_MultiplePortsSameHost(t *testing.T) {
|
||||||
|
m := AddrMatcher([]string{"example.com:80", "example.com:443"})
|
||||||
|
if !m.Match("example.com:80") {
|
||||||
|
t.Error("expected match for example.com:80 (first entry)")
|
||||||
|
}
|
||||||
|
if !m.Match("example.com:443") {
|
||||||
|
t.Error("expected match for example.com:443 (second entry, should not overwrite first)")
|
||||||
|
}
|
||||||
|
if m.Match("example.com:8080") {
|
||||||
|
t.Error("expected no match for example.com:8080")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddrMatcher_CaseInsensitive(t *testing.T) {
|
||||||
|
m := AddrMatcher([]string{"Example.COM:80"})
|
||||||
|
if !m.Match("EXAMPLE.COM:80") {
|
||||||
|
t.Error("expected case-insensitive match for EXAMPLE.COM:80")
|
||||||
|
}
|
||||||
|
if !m.Match("example.com:80") {
|
||||||
|
t.Error("expected case-insensitive match for example.com:80")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddrMatcher_CaseInsensitiveSubdomain(t *testing.T) {
|
||||||
|
m := AddrMatcher([]string{".Example.COM:80"})
|
||||||
|
if !m.Match("SUB.EXAMPLE.COM:80") {
|
||||||
|
t.Error("expected case-insensitive subdomain match")
|
||||||
|
}
|
||||||
|
if !m.Match("sub.example.com:80") {
|
||||||
|
t.Error("expected case-insensitive subdomain match (lowercased)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddrMatcher_EmptyAddrs(t *testing.T) {
|
||||||
|
m := AddrMatcher(nil)
|
||||||
|
if m.Match("example.com:80") {
|
||||||
|
t.Error("expected no match when no addrs configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddrMatcher_NilMatcher(t *testing.T) {
|
||||||
|
var m *addrMatcher
|
||||||
|
if m.Match("example.com:80") {
|
||||||
|
t.Error("expected no match on nil matcher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddrMatcher_IPv6WithPort(t *testing.T) {
|
||||||
|
m := AddrMatcher([]string{"[::1]:80"})
|
||||||
|
if !m.Match("[::1]:80") {
|
||||||
|
t.Error("expected match for [::1]:80")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CIDRMatcher ----
|
||||||
|
|
||||||
|
func TestCIDRMatcher_Match(t *testing.T) {
|
||||||
|
_, cidr1, _ := net.ParseCIDR("192.168.0.0/16")
|
||||||
|
m := CIDRMatcher([]*net.IPNet{cidr1})
|
||||||
|
if !m.Match("192.168.1.1") {
|
||||||
|
t.Error("expected match for 192.168.1.1 in 192.168.0.0/16")
|
||||||
|
}
|
||||||
|
if !m.Match("192.168.255.255") {
|
||||||
|
t.Error("expected match for 192.168.255.255 in 192.168.0.0/16")
|
||||||
|
}
|
||||||
|
if m.Match("10.0.0.1") {
|
||||||
|
t.Error("expected no match for 10.0.0.1")
|
||||||
|
}
|
||||||
|
if m.Match("not-an-ip") {
|
||||||
|
t.Error("expected no match for invalid IP")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCIDRMatcher_EmptyCIDRs(t *testing.T) {
|
||||||
|
m := CIDRMatcher(nil)
|
||||||
|
if m.Match("192.168.1.1") {
|
||||||
|
t.Error("expected no match when no CIDRs configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCIDRMatcher_NilMatcher(t *testing.T) {
|
||||||
|
var m *cidrMatcher
|
||||||
|
if m.Match("192.168.1.1") {
|
||||||
|
t.Error("expected no match on nil matcher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- DomainMatcher ----
|
||||||
|
|
||||||
|
func TestDomainMatcher_ExactMatch(t *testing.T) {
|
||||||
|
m := DomainMatcher([]string{"example.com", "test.org"})
|
||||||
|
if !m.Match("example.com") {
|
||||||
|
t.Error("expected match for example.com")
|
||||||
|
}
|
||||||
|
if !m.Match("test.org") {
|
||||||
|
t.Error("expected match for test.org")
|
||||||
|
}
|
||||||
|
if m.Match("other.com") {
|
||||||
|
t.Error("expected no match for other.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDomainMatcher_Subdomain(t *testing.T) {
|
||||||
|
m := DomainMatcher([]string{".example.com"})
|
||||||
|
if !m.Match("example.com") {
|
||||||
|
t.Error("expected match for example.com with .example.com rule")
|
||||||
|
}
|
||||||
|
if !m.Match("sub.example.com") {
|
||||||
|
t.Error("expected match for sub.example.com with .example.com rule")
|
||||||
|
}
|
||||||
|
if !m.Match("deep.sub.example.com") {
|
||||||
|
t.Error("expected match for deep.sub.example.com")
|
||||||
|
}
|
||||||
|
if m.Match("notexample.com") {
|
||||||
|
t.Error("expected no match for notexample.com (different domain)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDomainMatcher_NestedSubdomain(t *testing.T) {
|
||||||
|
m := DomainMatcher([]string{".example.com"})
|
||||||
|
if !m.Match("a.b.c.example.com") {
|
||||||
|
t.Error("expected match for deeply nested subdomain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDomainMatcher_CaseInsensitive(t *testing.T) {
|
||||||
|
m := DomainMatcher([]string{"Example.COM"})
|
||||||
|
if !m.Match("EXAMPLE.COM") {
|
||||||
|
t.Error("expected case-insensitive match for EXAMPLE.COM")
|
||||||
|
}
|
||||||
|
if !m.Match("example.com") {
|
||||||
|
t.Error("expected case-insensitive match for example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDomainMatcher_CaseInsensitiveSubdomain(t *testing.T) {
|
||||||
|
m := DomainMatcher([]string{".Example.COM"})
|
||||||
|
if !m.Match("SUB.EXAMPLE.COM") {
|
||||||
|
t.Error("expected case-insensitive subdomain match")
|
||||||
|
}
|
||||||
|
if !m.Match("sub.example.com") {
|
||||||
|
t.Error("expected case-insensitive subdomain match (lowercased)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDomainMatcher_EmptyDomains(t *testing.T) {
|
||||||
|
m := DomainMatcher(nil)
|
||||||
|
if m.Match("example.com") {
|
||||||
|
t.Error("expected no match when no domains configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDomainMatcher_NilMatcher(t *testing.T) {
|
||||||
|
var m *domainMatcher
|
||||||
|
if m.Match("example.com") {
|
||||||
|
t.Error("expected no match on nil matcher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- WildcardMatcher ----
|
||||||
|
|
||||||
|
func TestWildcardMatcher_Match(t *testing.T) {
|
||||||
|
m := WildcardMatcher([]string{"*.example.com"})
|
||||||
|
if !m.Match("foo.example.com") {
|
||||||
|
t.Error("expected match for foo.example.com")
|
||||||
|
}
|
||||||
|
if !m.Match("bar.example.com") {
|
||||||
|
t.Error("expected match for bar.example.com")
|
||||||
|
}
|
||||||
|
if m.Match("example.com") {
|
||||||
|
t.Error("expected no match for example.com (no wildcard prefix)")
|
||||||
|
}
|
||||||
|
if m.Match("foo.other.com") {
|
||||||
|
t.Error("expected no match for foo.other.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWildcardMatcher_WithPort(t *testing.T) {
|
||||||
|
m := WildcardMatcher([]string{"*.example.com:443"})
|
||||||
|
if !m.Match("foo.example.com:443") {
|
||||||
|
t.Error("expected match for foo.example.com:443")
|
||||||
|
}
|
||||||
|
if m.Match("foo.example.com:80") {
|
||||||
|
t.Error("expected no match for foo.example.com:80")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWildcardMatcher_WithPortRange(t *testing.T) {
|
||||||
|
m := WildcardMatcher([]string{"*.example.com:8000-9000"})
|
||||||
|
if !m.Match("foo.example.com:8000") {
|
||||||
|
t.Error("expected match for port 8000")
|
||||||
|
}
|
||||||
|
if !m.Match("foo.example.com:8500") {
|
||||||
|
t.Error("expected match for port 8500")
|
||||||
|
}
|
||||||
|
if m.Match("foo.example.com:7999") {
|
||||||
|
t.Error("expected no match for port 7999")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWildcardMatcher_CaseInsensitive(t *testing.T) {
|
||||||
|
m := WildcardMatcher([]string{"*.Example.COM"})
|
||||||
|
if !m.Match("FOO.EXAMPLE.COM") {
|
||||||
|
t.Error("expected case-insensitive match for FOO.EXAMPLE.COM")
|
||||||
|
}
|
||||||
|
if !m.Match("foo.example.com") {
|
||||||
|
t.Error("expected case-insensitive match for foo.example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWildcardMatcher_InvalidPattern(t *testing.T) {
|
||||||
|
// MustCompile would panic on this; Compile should just skip it.
|
||||||
|
m := WildcardMatcher([]string{"[invalid-glob"})
|
||||||
|
if m.Match("anything") {
|
||||||
|
t.Error("expected no match for invalid pattern")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWildcardMatcher_EmptyPatterns(t *testing.T) {
|
||||||
|
m := WildcardMatcher(nil)
|
||||||
|
if m.Match("foo.example.com") {
|
||||||
|
t.Error("expected no match when no patterns configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWildcardMatcher_NilMatcher(t *testing.T) {
|
||||||
|
var m *wildcardMatcher
|
||||||
|
if m.Match("foo.example.com") {
|
||||||
|
t.Error("expected no match on nil matcher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- IPRangeMatcher ----
|
||||||
|
|
||||||
|
func TestIPRangeMatcher_Match(t *testing.T) {
|
||||||
|
r := xnet.IPRange{}
|
||||||
|
if err := r.Parse("192.168.1.1-192.168.1.10"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m := IPRangeMatcher([]xnet.IPRange{r})
|
||||||
|
if !m.Match("192.168.1.1") {
|
||||||
|
t.Error("expected match for 192.168.1.1 in range")
|
||||||
|
}
|
||||||
|
if !m.Match("192.168.1.5") {
|
||||||
|
t.Error("expected match for 192.168.1.5 in range")
|
||||||
|
}
|
||||||
|
if !m.Match("192.168.1.10") {
|
||||||
|
t.Error("expected match for 192.168.1.10 in range")
|
||||||
|
}
|
||||||
|
if m.Match("192.168.1.11") {
|
||||||
|
t.Error("expected no match for 192.168.1.11")
|
||||||
|
}
|
||||||
|
if m.Match("10.0.0.1") {
|
||||||
|
t.Error("expected no match for 10.0.0.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIPRangeMatcher_WithPort(t *testing.T) {
|
||||||
|
r := xnet.IPRange{}
|
||||||
|
if err := r.Parse("192.168.1.1-192.168.1.10"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m := IPRangeMatcher([]xnet.IPRange{r})
|
||||||
|
if !m.Match("192.168.1.1:8080") {
|
||||||
|
t.Error("expected match for 192.168.1.1:8080 (port stripped)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIPRangeMatcher_EmptyRanges(t *testing.T) {
|
||||||
|
m := IPRangeMatcher(nil)
|
||||||
|
if m.Match("192.168.1.1") {
|
||||||
|
t.Error("expected no match when no ranges configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIPRangeMatcher_NilMatcher(t *testing.T) {
|
||||||
|
var m *ipRangeMatcher
|
||||||
|
if m.Match("192.168.1.1") {
|
||||||
|
t.Error("expected no match on nil matcher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- NopMatcher ----
|
||||||
|
|
||||||
|
func TestNopMatcher_Match(t *testing.T) {
|
||||||
|
m := NopMatcher()
|
||||||
|
if m.Match("anything") {
|
||||||
|
t.Error("nopMatcher should always return false")
|
||||||
|
}
|
||||||
|
if m.Match("") {
|
||||||
|
t.Error("nopMatcher should always return false for empty string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNopMatcher_NilMatcher(t *testing.T) {
|
||||||
|
var m *nopMatcher
|
||||||
|
if m.Match("anything") {
|
||||||
|
t.Error("expected no match on nil matcher")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Interface compliance ----
|
||||||
|
|
||||||
|
func TestMatcherInterface(t *testing.T) {
|
||||||
|
// All constructors must return Matcher interface values.
|
||||||
|
var _ Matcher = IPMatcher(nil)
|
||||||
|
var _ Matcher = AddrMatcher(nil)
|
||||||
|
var _ Matcher = CIDRMatcher(nil)
|
||||||
|
var _ Matcher = DomainMatcher(nil)
|
||||||
|
var _ Matcher = WildcardMatcher(nil)
|
||||||
|
var _ Matcher = IPRangeMatcher(nil)
|
||||||
|
var _ Matcher = NopMatcher()
|
||||||
|
}
|
||||||
@@ -8,6 +8,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ParseInterfaceAddr resolves ifceName to a network interface name and its addresses.
|
||||||
|
// If ifceName is an IP address, it finds the corresponding interface and returns
|
||||||
|
// a single address with port 0. If ifceName is an interface name, it returns all
|
||||||
|
// addresses assigned to that interface. The network parameter determines the address
|
||||||
|
// type (TCPAddr for "tcp"/"tcp4"/"tcp6", UDPAddr for "udp"/"udp4"/"udp6", IPAddr
|
||||||
|
// otherwise).
|
||||||
func ParseInterfaceAddr(ifceName, network string) (ifce string, addr []net.Addr, err error) {
|
func ParseInterfaceAddr(ifceName, network string) (ifce string, addr []net.Addr, err error) {
|
||||||
if ifceName == "" {
|
if ifceName == "" {
|
||||||
addr = append(addr, nil)
|
addr = append(addr, nil)
|
||||||
@@ -90,6 +96,7 @@ func findInterfaceByIP(ip net.IP) (string, error) {
|
|||||||
// e.g. 192.168.1.1:0-65535
|
// e.g. 192.168.1.1:0-65535
|
||||||
type AddrPortRange string
|
type AddrPortRange string
|
||||||
|
|
||||||
|
// Addrs expands the port range into individual "host:port" strings.
|
||||||
func (p AddrPortRange) Addrs() (addrs []string) {
|
func (p AddrPortRange) Addrs() (addrs []string) {
|
||||||
// ignore url scheme, e.g. http://, tls://, tcp://.
|
// ignore url scheme, e.g. http://, tls://, tcp://.
|
||||||
if strings.Contains(string(p), "://") {
|
if strings.Contains(string(p), "://") {
|
||||||
@@ -151,15 +158,18 @@ func (pr *PortRange) Parse(s string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contains reports whether port falls within the range.
|
||||||
func (pr *PortRange) Contains(port int) bool {
|
func (pr *PortRange) Contains(port int) bool {
|
||||||
return port >= pr.Min && port <= pr.Max
|
return port >= pr.Min && port <= pr.Max
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPRange represents a range of IP addresses.
|
||||||
type IPRange struct {
|
type IPRange struct {
|
||||||
Min netip.Addr
|
Min netip.Addr
|
||||||
Max netip.Addr
|
Max netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse parses s as a single IP or IP range "min-max" into r.
|
||||||
func (r *IPRange) Parse(s string) error {
|
func (r *IPRange) Parse(s string) error {
|
||||||
minmax := strings.Split(s, "-")
|
minmax := strings.Split(s, "-")
|
||||||
switch len(minmax) {
|
switch len(minmax) {
|
||||||
@@ -190,6 +200,7 @@ func (r *IPRange) Parse(s string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contains reports whether addr falls within the IP range.
|
||||||
func (r *IPRange) Contains(addr netip.Addr) bool {
|
func (r *IPRange) Contains(addr netip.Addr) bool {
|
||||||
return !(addr.Less(r.Min) || r.Max.Less(addr))
|
return !(addr.Less(r.Min) || r.Max.Less(addr))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// DefaultTimeout is the default dial timeout.
|
||||||
DefaultTimeout = 10 * time.Second
|
DefaultTimeout = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultNetDialer is the default Dialer used when a nil Dialer is provided.
|
||||||
var (
|
var (
|
||||||
DefaultNetDialer = &Dialer{}
|
DefaultNetDialer = &Dialer{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Dialer is a network dialer with support for interface binding, network
|
||||||
|
// namespace switching, and socket marking. The zero value is ready to use
|
||||||
|
// via DefaultNetDialer.
|
||||||
type Dialer struct {
|
type Dialer struct {
|
||||||
Interface string
|
Interface string
|
||||||
Netns string
|
Netns string
|
||||||
@@ -31,6 +36,10 @@ type Dialer struct {
|
|||||||
Log logger.Logger
|
Log logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dial connects to addr on the named network. If d is nil, DefaultNetDialer
|
||||||
|
// is used. When Netns is set, the dial switches into that network namespace
|
||||||
|
// before creating the connection. When Interface is set, it iterates the
|
||||||
|
// specified interfaces, trying each address in turn.
|
||||||
func (d *Dialer) Dial(ctx context.Context, network, addr string) (conn net.Conn, err error) {
|
func (d *Dialer) Dial(ctx context.Context, network, addr string) (conn net.Conn, err error) {
|
||||||
if d == nil {
|
if d == nil {
|
||||||
d = DefaultNetDialer
|
d = DefaultNetDialer
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetClientIP extracts the client IP from the request, checking CF-Connecting-IP,
|
||||||
|
// X-Forwarded-For, and X-Real-Ip headers in that order.
|
||||||
func GetClientIP(req *http.Request) net.IP {
|
func GetClientIP(req *http.Request) net.IP {
|
||||||
if req == nil {
|
if req == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -27,6 +29,7 @@ func GetClientIP(req *http.Request) net.IP {
|
|||||||
return net.ParseIP(sip)
|
return net.ParseIP(sip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Body wraps an io.ReadCloser with size-limited recording of the data read.
|
||||||
type Body struct {
|
type Body struct {
|
||||||
r io.ReadCloser
|
r io.ReadCloser
|
||||||
buf bytes.Buffer
|
buf bytes.Buffer
|
||||||
@@ -34,6 +37,7 @@ type Body struct {
|
|||||||
recordSize int
|
recordSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewBody creates a Body that reads from r and records up to maxRecordSize bytes.
|
||||||
func NewBody(r io.ReadCloser, maxRecordSize int) *Body {
|
func NewBody(r io.ReadCloser, maxRecordSize int) *Body {
|
||||||
p := &Body{
|
p := &Body{
|
||||||
r: r,
|
r: r,
|
||||||
@@ -63,10 +67,12 @@ func (p *Body) Close() error {
|
|||||||
return p.r.Close()
|
return p.r.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Content returns the recorded content.
|
||||||
func (p *Body) Content() []byte {
|
func (p *Body) Content() []byte {
|
||||||
return p.buf.Bytes()
|
return p.buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Length returns the total number of bytes read from the underlying reader.
|
||||||
func (p *Body) Length() int64 {
|
func (p *Body) Length() int64 {
|
||||||
return p.length
|
return p.length
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ var prots = map[waterutil.IPProtocol]string{
|
|||||||
waterutil.IPv6_ICMP: "IPv6-ICMP",
|
waterutil.IPv6_ICMP: "IPv6-ICMP",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Protocol returns the string name for the given IP protocol number.
|
||||||
func Protocol(p waterutil.IPProtocol) string {
|
func Protocol(p waterutil.IPProtocol) string {
|
||||||
if v, ok := prots[p]; ok {
|
if v, ok := prots[p]; ok {
|
||||||
return v
|
return v
|
||||||
|
|||||||
+14
-1
@@ -13,33 +13,41 @@ import (
|
|||||||
"github.com/vishvananda/netns"
|
"github.com/vishvananda/netns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SetBuffer is a connection that supports setting send and receive buffer sizes.
|
||||||
type SetBuffer interface {
|
type SetBuffer interface {
|
||||||
SetReadBuffer(bytes int) error
|
SetReadBuffer(bytes int) error
|
||||||
SetWriteBuffer(bytes int) error
|
SetWriteBuffer(bytes int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SyscallConn is a connection that exposes its raw system file descriptor.
|
||||||
type SyscallConn interface {
|
type SyscallConn interface {
|
||||||
SyscallConn() (syscall.RawConn, error)
|
SyscallConn() (syscall.RawConn, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoteAddr is a connection that exposes its remote address.
|
||||||
type RemoteAddr interface {
|
type RemoteAddr interface {
|
||||||
RemoteAddr() net.Addr
|
RemoteAddr() net.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
// tcpraw.TCPConn
|
// SetDSCP is a connection that supports setting the DSCP field.
|
||||||
type SetDSCP interface {
|
type SetDSCP interface {
|
||||||
SetDSCP(int) error
|
SetDSCP(int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsIPv4 reports whether address is an IPv4 address by checking whether
|
||||||
|
// it starts with ':' or '[' (IPv6) vs a digit (IPv4).
|
||||||
func IsIPv4(address string) bool {
|
func IsIPv4(address string) bool {
|
||||||
return address != "" && address[0] != ':' && address[0] != '['
|
return address != "" && address[0] != ':' && address[0] != '['
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListenConfig extends net.ListenConfig with network namespace support.
|
||||||
type ListenConfig struct {
|
type ListenConfig struct {
|
||||||
Netns string
|
Netns string
|
||||||
net.ListenConfig
|
net.ListenConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen announces on the local network address, switching into the configured
|
||||||
|
// network namespace first if one is set.
|
||||||
func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (net.Listener, error) {
|
func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (net.Listener, error) {
|
||||||
if lc.Netns != "" {
|
if lc.Netns != "" {
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
@@ -70,6 +78,8 @@ func (lc *ListenConfig) Listen(ctx context.Context, network, address string) (ne
|
|||||||
return lc.ListenConfig.Listen(ctx, network, address)
|
return lc.ListenConfig.Listen(ctx, network, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListenPacket announces on the local network address for packet connections,
|
||||||
|
// switching into the configured network namespace first if one is set.
|
||||||
func (lc *ListenConfig) ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error) {
|
func (lc *ListenConfig) ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error) {
|
||||||
if lc.Netns != "" {
|
if lc.Netns != "" {
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
@@ -105,6 +115,9 @@ type readWriteConn struct {
|
|||||||
w io.Writer
|
w io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewReadWriteConn returns a net.Conn that reads from r and writes to w,
|
||||||
|
// delegating all other operations to c. If c supports CloseRead or CloseWrite,
|
||||||
|
// those are forwarded; otherwise they return ErrUnsupported.
|
||||||
func NewReadWriteConn(r io.Reader, w io.Writer, c net.Conn) net.Conn {
|
func NewReadWriteConn(r io.Reader, w io.Writer, c net.Conn) net.Conn {
|
||||||
return &readWriteConn{
|
return &readWriteConn{
|
||||||
Conn: c,
|
Conn: c,
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ func WithReadTimeout(d time.Duration) PipeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pipe 在两个连接之间建立双向数据通道
|
// Pipe establishes a bidirectional data channel between two connections.
|
||||||
|
// Data read from rw1 is written to rw2, and data read from rw2 is written to
|
||||||
|
// rw1. Each direction runs in its own goroutine. Pipe returns the first
|
||||||
|
// non-EOF error encountered, or nil if both directions complete cleanly.
|
||||||
|
// When ctx is cancelled, both connections are forcefully closed.
|
||||||
func Pipe(ctx context.Context, rw1, rw2 io.ReadWriteCloser, opts ...PipeOption) error {
|
func Pipe(ctx context.Context, rw1, rw2 io.ReadWriteCloser, opts ...PipeOption) error {
|
||||||
var options pipeOptions
|
var options pipeOptions
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ func (c *serverConn) CloseWrite() error {
|
|||||||
return xio.ErrUnsupported
|
return xio.ErrUnsupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WrapClientConn writes a PROXY protocol header to c using src and dst
|
||||||
|
// addresses, then returns c for continued use. If ppv is <= 0 or either
|
||||||
|
// address is nil, the connection is returned unchanged.
|
||||||
func WrapClientConn(ppv int, src, dst net.Addr, c net.Conn) net.Conn {
|
func WrapClientConn(ppv int, src, dst net.Addr, c net.Conn) net.Conn {
|
||||||
if ppv <= 0 || c == nil {
|
if ppv <= 0 || c == nil {
|
||||||
return c
|
return c
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ func (ln *listener) Accept() (net.Conn, error) {
|
|||||||
return &serverConn{Conn: conn, ctx: innerCtx}, nil
|
return &serverConn{Conn: conn, ctx: innerCtx}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WrapListener wraps ln with a PROXY protocol listener that parses the PROXY
|
||||||
|
// header on each accepted connection. If ppv <= 0, ln is returned unchanged.
|
||||||
func WrapListener(ppv int, ln net.Listener, readHeaderTimeout time.Duration) net.Listener {
|
func WrapListener(ppv int, ln net.Listener, readHeaderTimeout time.Duration) net.Listener {
|
||||||
if ppv <= 0 {
|
if ppv <= 0 {
|
||||||
return ln
|
return ln
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import (
|
|||||||
ctxvalue "github.com/go-gost/x/ctx"
|
ctxvalue "github.com/go-gost/x/ctx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Resolve resolves addr to a concrete IP address. If hosts is non-nil, it is
|
||||||
|
// consulted first. If r is non-nil and no host mapping matched, the resolver
|
||||||
|
// is used. If neither is available, addr is returned unchanged.
|
||||||
func Resolve(ctx context.Context, network, addr string, r resolver.Resolver, hosts hosts.HostMapper, log logger.Logger) (string, error) {
|
func Resolve(ctx context.Context, network, addr string, r resolver.Resolver, hosts hosts.HostMapper, log logger.Logger) (string, error) {
|
||||||
if addr == "" {
|
if addr == "" {
|
||||||
return addr, nil
|
return addr, nil
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const (
|
|||||||
bufferSize = 64 * 1024
|
bufferSize = 64 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Transport copies data bidirectionally between rw1 and rw2 using two
|
||||||
|
// goroutines. It returns the first non-EOF error encountered.
|
||||||
func Transport(rw1, rw2 io.ReadWriter) error {
|
func Transport(rw1, rw2 io.ReadWriter) error {
|
||||||
errc := make(chan error, 2)
|
errc := make(chan error, 2)
|
||||||
go func() {
|
go func() {
|
||||||
@@ -27,6 +29,8 @@ func Transport(rw1, rw2 io.ReadWriter) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CopyBuffer copies from src to dst using a buffer of bufSize obtained from
|
||||||
|
// the buffer pool, reducing allocation overhead for repeated copies.
|
||||||
func CopyBuffer(dst io.Writer, src io.Reader, bufSize int) error {
|
func CopyBuffer(dst io.Writer, src io.Reader, bufSize int) error {
|
||||||
buf := bufpool.Get(bufSize)
|
buf := bufpool.Get(bufSize)
|
||||||
defer bufpool.Put(buf)
|
defer bufpool.Put(buf)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
xnet "github.com/go-gost/x/internal/net"
|
xnet "github.com/go-gost/x/internal/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Conn is a net.PacketConn that also supports io.Reader, io.Writer, buffer
|
||||||
|
// sizing, syscall access, and remote address exposure.
|
||||||
type Conn interface {
|
type Conn interface {
|
||||||
net.PacketConn
|
net.PacketConn
|
||||||
io.Reader
|
io.Reader
|
||||||
@@ -18,11 +20,13 @@ type Conn interface {
|
|||||||
xnet.RemoteAddr
|
xnet.RemoteAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadUDP supports reading UDP datagrams with metadata.
|
||||||
type ReadUDP interface {
|
type ReadUDP interface {
|
||||||
ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error)
|
ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error)
|
||||||
ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error)
|
ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteUDP supports writing UDP datagrams with metadata.
|
||||||
type WriteUDP interface {
|
type WriteUDP interface {
|
||||||
WriteToUDP(b []byte, addr *net.UDPAddr) (int, error)
|
WriteToUDP(b []byte, addr *net.UDPAddr) (int, error)
|
||||||
WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error)
|
WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/go-gost/core/logger"
|
"github.com/go-gost/core/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ListenConfig configures a UDP-to-TCP-stream listener.
|
||||||
type ListenConfig struct {
|
type ListenConfig struct {
|
||||||
Addr net.Addr
|
Addr net.Addr
|
||||||
Backlog int
|
Backlog int
|
||||||
@@ -29,6 +30,9 @@ type listener struct {
|
|||||||
config *ListenConfig
|
config *ListenConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewListener creates a net.Listener from a net.PacketConn by demultiplexing
|
||||||
|
// UDP datagrams into per-client net.Conn streams. Idle connections are cleaned
|
||||||
|
// up according to cfg.TTL.
|
||||||
func NewListener(conn net.PacketConn, cfg *ListenConfig) net.Listener {
|
func NewListener(conn net.PacketConn, cfg *ListenConfig) net.Listener {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = &ListenConfig{}
|
cfg = &ListenConfig{}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const (
|
|||||||
defaultBufferSize = 4096
|
defaultBufferSize = 4096
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Relay copies UDP datagrams between two net.PacketConns bidirectionally.
|
||||||
type Relay struct {
|
type Relay struct {
|
||||||
service string
|
service string
|
||||||
pc1 net.PacketConn
|
pc1 net.PacketConn
|
||||||
@@ -22,6 +23,7 @@ type Relay struct {
|
|||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewRelay creates a Relay that copies datagrams between pc1 and pc2.
|
||||||
func NewRelay(pc1, pc2 net.PacketConn) *Relay {
|
func NewRelay(pc1, pc2 net.PacketConn) *Relay {
|
||||||
return &Relay{
|
return &Relay{
|
||||||
pc1: pc1,
|
pc1: pc1,
|
||||||
@@ -29,26 +31,31 @@ func NewRelay(pc1, pc2 net.PacketConn) *Relay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithService sets the service name for bypass matching.
|
||||||
func (r *Relay) WithService(service string) *Relay {
|
func (r *Relay) WithService(service string) *Relay {
|
||||||
r.service = service
|
r.service = service
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithBypass sets the bypass matcher to skip certain addresses.
|
||||||
func (r *Relay) WithBypass(bp bypass.Bypass) *Relay {
|
func (r *Relay) WithBypass(bp bypass.Bypass) *Relay {
|
||||||
r.bypass = bp
|
r.bypass = bp
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithLogger sets the logger.
|
||||||
func (r *Relay) WithLogger(logger logger.Logger) *Relay {
|
func (r *Relay) WithLogger(logger logger.Logger) *Relay {
|
||||||
r.logger = logger
|
r.logger = logger
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithBufferSize sets the buffer size for copy operations.
|
||||||
func (r *Relay) WithBufferSize(n int) *Relay {
|
func (r *Relay) WithBufferSize(n int) *Relay {
|
||||||
r.bufferSize = n
|
r.bufferSize = n
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run starts the relay. It blocks until an error occurs or ctx is cancelled.
|
||||||
func (r *Relay) Run(ctx context.Context) (err error) {
|
func (r *Relay) Run(ctx context.Context) (err error) {
|
||||||
errc := make(chan error, 2)
|
errc := make(chan error, 2)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Package plugin provides shared utilities for building gRPC and HTTP plugin
|
||||||
|
// clients. Both transport types support authentication tokens, TLS
|
||||||
|
// configuration, custom headers (HTTP), and connection timeouts.
|
||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -13,10 +16,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// GRPC is the transport type identifier for gRPC-based plugins.
|
||||||
GRPC string = "grpc"
|
GRPC string = "grpc"
|
||||||
|
// HTTP is the transport type identifier for HTTP-based plugins.
|
||||||
HTTP string = "http"
|
HTTP string = "http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Options holds the common configuration for gRPC and HTTP plugin clients.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Token string
|
Token string
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
@@ -24,32 +30,40 @@ type Options struct {
|
|||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Option configures Options.
|
||||||
type Option func(opts *Options)
|
type Option func(opts *Options)
|
||||||
|
|
||||||
|
// TokenOption sets the authentication token sent with each request.
|
||||||
func TokenOption(token string) Option {
|
func TokenOption(token string) Option {
|
||||||
return func(opts *Options) {
|
return func(opts *Options) {
|
||||||
opts.Token = token
|
opts.Token = token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TLSConfigOption sets the TLS configuration for transport security.
|
||||||
func TLSConfigOption(cfg *tls.Config) Option {
|
func TLSConfigOption(cfg *tls.Config) Option {
|
||||||
return func(opts *Options) {
|
return func(opts *Options) {
|
||||||
opts.TLSConfig = cfg
|
opts.TLSConfig = cfg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HeaderOption sets custom HTTP headers sent with each request (HTTP transport only).
|
||||||
func HeaderOption(header http.Header) Option {
|
func HeaderOption(header http.Header) Option {
|
||||||
return func(opts *Options) {
|
return func(opts *Options) {
|
||||||
opts.Header = header
|
opts.Header = header
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TimeoutOption sets the request timeout (HTTP transport only).
|
||||||
func TimeoutOption(timeout time.Duration) Option {
|
func TimeoutOption(timeout time.Duration) Option {
|
||||||
return func(opts *Options) {
|
return func(opts *Options) {
|
||||||
opts.Timeout = timeout
|
opts.Timeout = timeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewGRPCConn creates a gRPC client connection to addr. If opts is nil or
|
||||||
|
// opts.TLSConfig is nil, the connection uses insecure transport. When
|
||||||
|
// opts.Token is set, it is attached as per-RPC credentials.
|
||||||
func NewGRPCConn(addr string, opts *Options) (*grpc.ClientConn, error) {
|
func NewGRPCConn(addr string, opts *Options) (*grpc.ClientConn, error) {
|
||||||
grpcOpts := []grpc.DialOption{
|
grpcOpts := []grpc.DialOption{
|
||||||
// grpc.WithBlock(),
|
// grpc.WithBlock(),
|
||||||
@@ -57,6 +71,10 @@ func NewGRPCConn(addr string, opts *Options) (*grpc.ClientConn, error) {
|
|||||||
Backoff: backoff.DefaultConfig,
|
Backoff: backoff.DefaultConfig,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
if opts == nil {
|
||||||
|
grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
|
return grpc.NewClient(addr, grpcOpts...)
|
||||||
|
}
|
||||||
if opts.TLSConfig != nil {
|
if opts.TLSConfig != nil {
|
||||||
grpcOpts = append(grpcOpts,
|
grpcOpts = append(grpcOpts,
|
||||||
grpc.WithAuthority(opts.TLSConfig.ServerName),
|
grpc.WithAuthority(opts.TLSConfig.ServerName),
|
||||||
@@ -85,7 +103,14 @@ func (c *rpcCredentials) RequireTransportSecurity() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewHTTPClient creates an http.Client from opts. If opts is nil, a default
|
||||||
|
// client with no timeout is returned. Use HTTPClientTransport to access the
|
||||||
|
// underlying transport and close idle connections when the client is no longer
|
||||||
|
// needed.
|
||||||
func NewHTTPClient(opts *Options) *http.Client {
|
func NewHTTPClient(opts *Options) *http.Client {
|
||||||
|
if opts == nil {
|
||||||
|
return &http.Client{}
|
||||||
|
}
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Timeout: opts.Timeout,
|
Timeout: opts.Timeout,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
@@ -93,3 +118,15 @@ func NewHTTPClient(opts *Options) *http.Client {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTPClientTransport returns the *http.Transport underlying c, or nil if c's
|
||||||
|
// transport is not an *http.Transport. Callers that hold the client for a
|
||||||
|
// long time should call tr.CloseIdleConnections() when the client is no longer
|
||||||
|
// needed.
|
||||||
|
func HTTPClientTransport(c *http.Client) *http.Transport {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tr, _ := c.Transport.(*http.Transport)
|
||||||
|
return tr
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewGRPCConn_NilOpts(t *testing.T) {
|
||||||
|
conn, err := NewGRPCConn("localhost:0", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if conn == nil {
|
||||||
|
t.Fatal("expected non-nil conn")
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewGRPCConn_NoTLS(t *testing.T) {
|
||||||
|
conn, err := NewGRPCConn("localhost:0", &Options{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if conn == nil {
|
||||||
|
t.Fatal("expected non-nil conn")
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewGRPCConn_WithTLS(t *testing.T) {
|
||||||
|
conn, err := NewGRPCConn("localhost:0", &Options{
|
||||||
|
TLSConfig: nil, // nil TLSConfig -> insecure
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if conn == nil {
|
||||||
|
t.Fatal("expected non-nil conn")
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHTTPClient_NilOpts(t *testing.T) {
|
||||||
|
c := NewHTTPClient(nil)
|
||||||
|
if c == nil {
|
||||||
|
t.Fatal("expected non-nil client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHTTPClient_Defaults(t *testing.T) {
|
||||||
|
c := NewHTTPClient(&Options{})
|
||||||
|
if c == nil {
|
||||||
|
t.Fatal("expected non-nil client")
|
||||||
|
}
|
||||||
|
if c.Timeout != 0 {
|
||||||
|
t.Errorf("expected zero timeout, got %v", c.Timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHTTPClient_WithTimeout(t *testing.T) {
|
||||||
|
c := NewHTTPClient(&Options{Timeout: 5 * time.Second})
|
||||||
|
if c.Timeout != 5*time.Second {
|
||||||
|
t.Errorf("expected 5s timeout, got %v", c.Timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPClientTransport_Nil(t *testing.T) {
|
||||||
|
if tr := HTTPClientTransport(nil); tr != nil {
|
||||||
|
t.Error("expected nil transport for nil client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPClientTransport_Valid(t *testing.T) {
|
||||||
|
c := NewHTTPClient(&Options{})
|
||||||
|
tr := HTTPClientTransport(c)
|
||||||
|
if tr == nil {
|
||||||
|
t.Fatal("expected non-nil transport")
|
||||||
|
}
|
||||||
|
// Verify we can call CloseIdleConnections without panic.
|
||||||
|
tr.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOption(t *testing.T) {
|
||||||
|
opts := &Options{}
|
||||||
|
TokenOption("tok")(opts)
|
||||||
|
TLSConfigOption(nil)(opts)
|
||||||
|
HeaderOption(http.Header{"X": []string{"y"}})(opts)
|
||||||
|
TimeoutOption(time.Second)(opts)
|
||||||
|
|
||||||
|
if opts.Token != "tok" {
|
||||||
|
t.Errorf("expected Token 'tok', got %q", opts.Token)
|
||||||
|
}
|
||||||
|
if opts.Timeout != time.Second {
|
||||||
|
t.Errorf("expected 1s timeout, got %v", opts.Timeout)
|
||||||
|
}
|
||||||
|
if opts.Header.Get("X") != "y" {
|
||||||
|
t.Errorf("expected header X=y, got %v", opts.Header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHTTPClient_TransportTLSConfig(t *testing.T) {
|
||||||
|
// nil TLSConfig — transport should still exist.
|
||||||
|
c := NewHTTPClient(&Options{})
|
||||||
|
tr := HTTPClientTransport(c)
|
||||||
|
if tr == nil {
|
||||||
|
t.Fatal("expected non-nil transport")
|
||||||
|
}
|
||||||
|
if tr.TLSClientConfig != nil {
|
||||||
|
t.Error("expected nil TLSClientConfig")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user