Browse Source

[app/dispatcher] [proxy/dns] Support domain string validation (#2188)

Vigilans 2 years ago
parent
commit
ac0d9480bd
4 changed files with 120 additions and 25 deletions
  1. 6 4
      app/dispatcher/default.go
  2. 33 19
      common/strmatcher/matchers.go
  3. 76 0
      common/strmatcher/matchers_test.go
  4. 5 2
      proxy/dns/dns.go

+ 6 - 4
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)
 		}()

+ 33 - 19
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")
+			}
+		}
+		if !isASCII {
+			var err error
+			pattern, err = idna.New().ToASCII(pattern)
+			if err != nil {
+				return "", err
+			}
+			continue
 		}
-		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 hasUpper {
+			pattern = strings.ToLower(pattern)
 		}
-		builder.WriteByte(c)
+		break
 	}
-	return builder.String(), nil
+	return pattern, nil
 }
 
 // MatcherGroupForAll is an interface indicating a MatcherGroup could accept all types of matchers.

+ 76 - 0
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)
+		}
+	}
+}

+ 5 - 2
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
+					}
 				}
 			}