From ac0d9480bd2f63f43afc1ad5fc9a5d0a3cabd358 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Sat, 10 Dec 2022 17:07:59 +0800 Subject: [PATCH] [app/dispatcher] [proxy/dns] Support domain string validation (#2188) --- app/dispatcher/default.go | 10 ++-- common/strmatcher/matchers.go | 52 ++++++++++++-------- common/strmatcher/matchers_test.go | 76 ++++++++++++++++++++++++++++++ proxy/dns/dns.go | 7 ++- 4 files changed, 120 insertions(+), 25 deletions(-) diff --git a/app/dispatcher/default.go b/app/dispatcher/default.go index e1fd16a70..2cb3cce43 100644 --- a/app/dispatcher/default.go +++ b/app/dispatcher/default.go @@ -15,6 +15,7 @@ import ( "github.com/v2fly/v2ray-core/v5/common/net" "github.com/v2fly/v2ray-core/v5/common/protocol" "github.com/v2fly/v2ray-core/v5/common/session" + "github.com/v2fly/v2ray-core/v5/common/strmatcher" "github.com/v2fly/v2ray-core/v5/features/outbound" "github.com/v2fly/v2ray-core/v5/features/policy" "github.com/v2fly/v2ray-core/v5/features/routing" @@ -224,10 +225,11 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin content.Protocol = result.Protocol() } if err == nil && shouldOverride(result, sniffingRequest.OverrideDestinationForProtocol) { - domain := result.Domain() - newError("sniffed domain: ", domain).WriteToLog(session.ExportIDToError(ctx)) - destination.Address = net.ParseAddress(domain) - ob.Target = destination + if domain, err := strmatcher.ToDomain(result.Domain()); err == nil { + newError("sniffed domain: ", domain, " for ", destination).WriteToLog(session.ExportIDToError(ctx)) + destination.Address = net.ParseAddress(domain) + ob.Target = destination + } } d.routedDispatch(ctx, outbound, destination) }() diff --git a/common/strmatcher/matchers.go b/common/strmatcher/matchers.go index f13756fa4..188343401 100644 --- a/common/strmatcher/matchers.go +++ b/common/strmatcher/matchers.go @@ -5,6 +5,8 @@ import ( "regexp" "strings" "unicode/utf8" + + "golang.org/x/net/idna" ) // FullMatcher is an implementation of Matcher. @@ -151,29 +153,41 @@ func (t Type) NewDomainPattern(pattern string) (Matcher, error) { // * Letters A to Z (no distinction between uppercase and lowercase, we convert to lowers) // * Digits 0 to 9 // * Hyphens(-) and Periods(.) -// 2. Non-ASCII characters not supported for now. -// * May support Internationalized domain name to Punycode if needed in the future. +// 2. If any non-ASCII characters, domain are converted from Internationalized domain name to Punycode. func ToDomain(pattern string) (string, error) { - builder := strings.Builder{} - builder.Grow(len(pattern)) - for i := 0; i < len(pattern); i++ { - c := pattern[i] - if c >= utf8.RuneSelf { - return "", errors.New("non-ASCII characters not supported for now") + for { + isASCII, hasUpper := true, false + for i := 0; i < len(pattern); i++ { + c := pattern[i] + if c >= utf8.RuneSelf { + isASCII = false + break + } + switch { + case 'A' <= c && c <= 'Z': + hasUpper = true + case 'a' <= c && c <= 'z': + case '0' <= c && c <= '9': + case c == '-': + case c == '.': + default: + return "", errors.New("pattern string does not conform to Letter-Digit-Hyphen (LDH) subset") + } } - switch { - case 'A' <= c && c <= 'Z': - c += 'a' - 'A' - case 'a' <= c && c <= 'z': - case '0' <= c && c <= '9': - case c == '-': - case c == '.': - default: - return "", errors.New("pattern string does not conform to Letter-Digit-Hyphen (LDH) subset") + if !isASCII { + var err error + pattern, err = idna.New().ToASCII(pattern) + if err != nil { + return "", err + } + continue } - builder.WriteByte(c) + if hasUpper { + pattern = strings.ToLower(pattern) + } + break } - return builder.String(), nil + return pattern, nil } // MatcherGroupForAll is an interface indicating a MatcherGroup could accept all types of matchers. diff --git a/common/strmatcher/matchers_test.go b/common/strmatcher/matchers_test.go index 587575e04..f85674219 100644 --- a/common/strmatcher/matchers_test.go +++ b/common/strmatcher/matchers_test.go @@ -1,7 +1,9 @@ package strmatcher_test import ( + "reflect" "testing" + "unsafe" "github.com/v2fly/v2ray-core/v5/common" . "github.com/v2fly/v2ray-core/v5/common/strmatcher" @@ -71,3 +73,77 @@ func TestMatcher(t *testing.T) { } } } + +func TestToDomain(t *testing.T) { + { // Test normal ASCII domain, which should not trigger new string data allocation + input := "v2fly.org" + domain, err := ToDomain(input) + if err != nil { + t.Error("unexpected error: ", err) + } + if domain != input { + t.Error("unexpected output: ", domain, " for test case ", input) + } + if (*reflect.StringHeader)(unsafe.Pointer(&input)).Data != (*reflect.StringHeader)(unsafe.Pointer(&domain)).Data { + t.Error("different string data of output: ", domain, " and test case ", input) + } + } + { // Test ASCII domain containing upper case letter, which should be converted to lower case + input := "v2FLY.oRg" + domain, err := ToDomain(input) + if err != nil { + t.Error("unexpected error: ", err) + } + if domain != "v2fly.org" { + t.Error("unexpected output: ", domain, " for test case ", input) + } + } + { // Test internationalized domain, which should be translated to ASCII punycode + input := "v2fly.公益" + domain, err := ToDomain(input) + if err != nil { + t.Error("unexpected error: ", err) + } + if domain != "v2fly.xn--55qw42g" { + t.Error("unexpected output: ", domain, " for test case ", input) + } + } + { // Test internationalized domain containing upper case letter + input := "v2FLY.公益" + domain, err := ToDomain(input) + if err != nil { + t.Error("unexpected error: ", err) + } + if domain != "v2fly.xn--55qw42g" { + t.Error("unexpected output: ", domain, " for test case ", input) + } + } + { // Test domain name of invalid character, which should return with error + input := "{" + _, err := ToDomain(input) + if err == nil { + t.Error("unexpected non error for test case ", input) + } + } + { // Test domain name containing a space, which should return with error + input := "Mijia Cloud" + _, err := ToDomain(input) + if err == nil { + t.Error("unexpected non error for test case ", input) + } + } + { // Test domain name containing an underscore, which should return with error + input := "Mijia_Cloud.com" + _, err := ToDomain(input) + if err == nil { + t.Error("unexpected non error for test case ", input) + } + } + { // Test internationalized domain containing invalid character + input := "Mijia Cloud.公司" + _, err := ToDomain(input) + if err == nil { + t.Error("unexpected non error for test case ", input) + } + } +} diff --git a/proxy/dns/dns.go b/proxy/dns/dns.go index c6b2837d8..287996bc1 100644 --- a/proxy/dns/dns.go +++ b/proxy/dns/dns.go @@ -15,6 +15,7 @@ import ( dns_proto "github.com/v2fly/v2ray-core/v5/common/protocol/dns" "github.com/v2fly/v2ray-core/v5/common/session" "github.com/v2fly/v2ray-core/v5/common/signal" + "github.com/v2fly/v2ray-core/v5/common/strmatcher" "github.com/v2fly/v2ray-core/v5/common/task" "github.com/v2fly/v2ray-core/v5/features/dns" "github.com/v2fly/v2ray-core/v5/features/policy" @@ -190,8 +191,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet. if !h.isOwnLink(ctx) { isIPQuery, domain, id, qType := parseIPQuery(b.Bytes()) if isIPQuery { - go h.handleIPQuery(id, qType, domain, writer) - continue + if domain, err := strmatcher.ToDomain(domain); err == nil { + go h.handleIPQuery(id, qType, domain, writer) + continue + } } }