Merge branch 'apernet:master' into master

This commit is contained in:
Xboard 2023-11-24 22:23:09 +08:00 committed by GitHub
commit abe1d79b46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 167 additions and 43 deletions

View file

@ -172,13 +172,13 @@ func (c *clientImpl) openStream() (quic.Stream, error) {
func (c *clientImpl) TCP(addr string) (net.Conn, error) {
stream, err := c.openStream()
if err != nil {
return nil, maybeWrapQUICClosedError(err)
return nil, wrapIfConnectionClosed(err)
}
// Send request
err = protocol.WriteTCPRequest(stream, addr)
if err != nil {
_ = stream.Close()
return nil, maybeWrapQUICClosedError(err)
return nil, wrapIfConnectionClosed(err)
}
if c.config.FastOpen {
// Don't wait for the response when fast open is enabled.
@ -195,7 +195,7 @@ func (c *clientImpl) TCP(addr string) (net.Conn, error) {
ok, msg, err := protocol.ReadTCPResponse(stream)
if err != nil {
_ = stream.Close()
return nil, maybeWrapQUICClosedError(err)
return nil, wrapIfConnectionClosed(err)
}
if !ok {
_ = stream.Close()
@ -222,12 +222,14 @@ func (c *clientImpl) Close() error {
return nil
}
// maybeWrapQUICClosedError checks if the error returned by quic-go
// indicates that the QUIC connection is permanently closed,
// and if so, wraps it with coreErrs.ClosedError.
func maybeWrapQUICClosedError(err error) error {
// wrapIfConnectionClosed checks if the error returned by quic-go
// indicates that the QUIC connection has been permanently closed,
// and if so, wraps the error with coreErrs.ClosedError.
// PITFALL: sometimes quic-go has "internal errors" that are not net.Error,
// but we still need to treat them as ClosedError.
func wrapIfConnectionClosed(err error) error {
netErr, ok := err.(net.Error)
if ok && !netErr.Temporary() {
if !ok || !netErr.Temporary() {
return coreErrs.ClosedError{Err: err}
} else {
return err

View file

@ -236,6 +236,17 @@ func compileHostMatcher(addr string, geoLoader GeoLoader) (hostMatcher, string)
}
return m, ""
}
if strings.HasPrefix(addr, "suffix:") {
// Domain suffix matcher
suffix := addr[7:]
if len(suffix) == 0 {
return nil, "empty domain suffix"
}
return &domainMatcher{
Pattern: suffix,
Mode: domainMatchSuffix,
}, ""
}
if strings.Contains(addr, "/") {
// CIDR matcher
_, ipnet, err := net.ParseCIDR(addr)
@ -251,14 +262,14 @@ func compileHostMatcher(addr string, geoLoader GeoLoader) (hostMatcher, string)
if strings.Contains(addr, "*") {
// Wildcard domain matcher
return &domainMatcher{
Pattern: addr,
Wildcard: true,
Pattern: addr,
Mode: domainMatchWildcard,
}, ""
}
// Nothing else matched, treat it as a non-wildcard domain
return &domainMatcher{
Pattern: addr,
Wildcard: false,
Pattern: addr,
Mode: domainMatchExact,
}, ""
}

View file

@ -22,7 +22,7 @@ func (l *testGeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) {
}
func TestCompile(t *testing.T) {
ob1, ob2, ob3, ob4 := 1, 2, 3, 4
ob1, ob2, ob3, ob4, ob5 := 1, 2, 3, 4, 5
rules := []TextRule{
{
Outbound: "ob1",
@ -84,12 +84,19 @@ func TestCompile(t *testing.T) {
ProtoPort: "*/*",
HijackAddress: "",
},
{
Outbound: "ob5",
Address: "suffix:microsoft.com",
ProtoPort: "*/*",
HijackAddress: "",
},
}
comp, err := Compile[int](rules, map[string]int{
"ob1": ob1,
"ob2": ob2,
"ob3": ob3,
"ob4": ob4,
"ob5": ob5,
}, 100, &testGeoLoader{})
assert.NoError(t, err)
@ -208,6 +215,33 @@ func TestCompile(t *testing.T) {
wantOutbound: 0, // no match default
wantIP: nil,
},
{
host: HostInfo{
Name: "microsoft.com",
},
proto: ProtocolTCP,
port: 6000,
wantOutbound: ob5,
wantIP: nil,
},
{
host: HostInfo{
Name: "real.microsoft.com",
},
proto: ProtocolUDP,
port: 5353,
wantOutbound: ob5,
wantIP: nil,
},
{
host: HostInfo{
Name: "fakemicrosoft.com",
},
proto: ProtocolTCP,
port: 5000,
wantOutbound: 0, // no match default
wantIP: nil,
},
}
for _, test := range tests {

View file

@ -2,10 +2,17 @@ package acl
import (
"net"
"strings"
"golang.org/x/net/idna"
)
const (
domainMatchExact = uint8(iota)
domainMatchWildcard
domainMatchSuffix
)
type hostMatcher interface {
Match(HostInfo) bool
}
@ -27,8 +34,8 @@ func (m *cidrMatcher) Match(host HostInfo) bool {
}
type domainMatcher struct {
Pattern string
Wildcard bool
Pattern string
Mode uint8
}
func (m *domainMatcher) Match(host HostInfo) bool {
@ -36,10 +43,16 @@ func (m *domainMatcher) Match(host HostInfo) bool {
if err != nil {
name = host.Name
}
if m.Wildcard {
switch m.Mode {
case domainMatchExact:
return name == m.Pattern
case domainMatchWildcard:
return deepMatchRune([]rune(name), []rune(m.Pattern))
case domainMatchSuffix:
return name == m.Pattern || strings.HasSuffix(name, "."+m.Pattern)
default:
return false // Invalid mode
}
return name == m.Pattern
}
func deepMatchRune(str, pattern []rune) bool {

View file

@ -142,8 +142,8 @@ func Test_cidrMatcher_Match(t *testing.T) {
func Test_domainMatcher_Match(t *testing.T) {
type fields struct {
Pattern string
Wildcard bool
Pattern string
Mode uint8
}
tests := []struct {
name string
@ -154,8 +154,8 @@ func Test_domainMatcher_Match(t *testing.T) {
{
name: "non-wildcard match",
fields: fields{
Pattern: "example.com",
Wildcard: false,
Pattern: "example.com",
Mode: domainMatchExact,
},
host: HostInfo{
Name: "example.com",
@ -165,8 +165,8 @@ func Test_domainMatcher_Match(t *testing.T) {
{
name: "non-wildcard IDN match",
fields: fields{
Pattern: "政府.中国",
Wildcard: false,
Pattern: "政府.中国",
Mode: domainMatchExact,
},
host: HostInfo{
Name: "xn--mxtq1m.xn--fiqs8s",
@ -176,8 +176,8 @@ func Test_domainMatcher_Match(t *testing.T) {
{
name: "non-wildcard no match",
fields: fields{
Pattern: "example.com",
Wildcard: false,
Pattern: "example.com",
Mode: domainMatchExact,
},
host: HostInfo{
Name: "example.org",
@ -187,8 +187,8 @@ func Test_domainMatcher_Match(t *testing.T) {
{
name: "non-wildcard IDN no match",
fields: fields{
Pattern: "政府.中国",
Wildcard: false,
Pattern: "政府.中国",
Mode: domainMatchExact,
},
host: HostInfo{
Name: "xn--mxtq1m.xn--yfro4i67o",
@ -198,8 +198,8 @@ func Test_domainMatcher_Match(t *testing.T) {
{
name: "wildcard match 1",
fields: fields{
Pattern: "*.example.com",
Wildcard: true,
Pattern: "*.example.com",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "www.example.com",
@ -209,8 +209,8 @@ func Test_domainMatcher_Match(t *testing.T) {
{
name: "wildcard match 2",
fields: fields{
Pattern: "example*.com",
Wildcard: true,
Pattern: "example*.com",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "example2.com",
@ -220,8 +220,8 @@ func Test_domainMatcher_Match(t *testing.T) {
{
name: "wildcard IDN match 1",
fields: fields{
Pattern: "战狼*.com",
Wildcard: true,
Pattern: "战狼*.com",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "xn--2-x14by21c.com",
@ -231,8 +231,8 @@ func Test_domainMatcher_Match(t *testing.T) {
{
name: "wildcard IDN match 2",
fields: fields{
Pattern: "*大学*",
Wildcard: true,
Pattern: "*大学*",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "xn--xkry9kk1bz66a.xn--ses554g",
@ -242,8 +242,8 @@ func Test_domainMatcher_Match(t *testing.T) {
{
name: "wildcard no match",
fields: fields{
Pattern: "*.example.com",
Wildcard: true,
Pattern: "*.example.com",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "example.com",
@ -253,19 +253,83 @@ func Test_domainMatcher_Match(t *testing.T) {
{
name: "wildcard IDN no match",
fields: fields{
Pattern: "*呵呵*",
Wildcard: true,
Pattern: "*呵呵*",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "xn--6qqt7juua.cn",
},
want: false,
},
{
name: "suffix match 1",
fields: fields{
Pattern: "apple.com",
Mode: domainMatchSuffix,
},
host: HostInfo{
Name: "apple.com",
},
want: true,
},
{
name: "suffix match 2",
fields: fields{
Pattern: "apple.com",
Mode: domainMatchSuffix,
},
host: HostInfo{
Name: "store.apple.com",
},
want: true,
},
{
name: "suffix IDN match 1",
fields: fields{
Pattern: "中国",
Mode: domainMatchSuffix,
},
host: HostInfo{
Name: "中国",
},
want: true,
},
{
name: "suffix IDN match 2",
fields: fields{
Pattern: "中国",
Mode: domainMatchSuffix,
},
host: HostInfo{
Name: "天安门.中国",
},
want: true,
},
{
name: "suffix no match",
fields: fields{
Pattern: "news.com",
},
host: HostInfo{
Name: "fakenews.com",
},
want: false,
},
{
name: "suffix IDN no match",
fields: fields{
Pattern: "冲浪",
},
host: HostInfo{
Name: "666.网上冲浪",
},
want: false,
},
{
name: "empty",
fields: fields{
Pattern: "*.example.com",
Wildcard: true,
Pattern: "*.example.com",
Mode: domainMatchWildcard,
},
host: HostInfo{
Name: "",
@ -276,8 +340,8 @@ func Test_domainMatcher_Match(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &domainMatcher{
Pattern: tt.fields.Pattern,
Wildcard: tt.fields.Wildcard,
Pattern: tt.fields.Pattern,
Mode: tt.fields.Mode,
}
if got := m.Match(tt.host); got != tt.want {
t.Errorf("Match() = %v, want %v", got, tt.want)