Repository: v2fly/v2ray-core
Pull request #3102 (2024-08-03 12:05:19)

Fix HTTP2 proxy client with uTLS
`iConn.(*tls.Conn)` will fail for a uTLS Conn. Use `security.ConnectionApplicationProtocol` to get ALPN instead.


Patch:
diff --git a/app/dns/nameserver_doh.go b/app/dns/nameserver_doh.go
index 6e1430e6..b1889c16 100644
--- a/app/dns/nameserver_doh.go
+++ b/app/dns/nameserver_doh.go
@@ -11,7 +11,6 @@ import (
 	"net/http"
 	"net/url"
 	"sync"
-	"sync/atomic"
 	"time"
 
 	"golang.org/x/net/dns/dnsmessage"
@@ -35,7 +34,6 @@ type DoHNameServer struct {
 	ips        map[string]record
 	pub        *pubsub.Service
 	cleanup    *task.Periodic
-	reqID      uint32
 	httpClient *http.Client
 	dohURL     string
 	name       string
@@ -204,7 +202,7 @@ func (s *DoHNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) {
 }
 
 func (s *DoHNameServer) newReqID() uint16 {
-	return uint16(atomic.AddUint32(&s.reqID, 1))
+	return 0
 }
 
 func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) {
diff --git a/app/dns/nameserver_quic.go b/app/dns/nameserver_quic.go
index 28e69225..7275cb8a 100644
--- a/app/dns/nameserver_quic.go
+++ b/app/dns/nameserver_quic.go
@@ -6,7 +6,6 @@ import (
 	"encoding/binary"
 	"net/url"
 	"sync"
-	"sync/atomic"
 	"time"
 
 	"github.com/quic-go/quic-go"
@@ -35,7 +34,6 @@ type QUICNameServer struct {
 	ips         map[string]record
 	pub         *pubsub.Service
 	cleanup     *task.Periodic
-	reqID       uint32
 	name        string
 	destination net.Destination
 	connection  quic.Connection
@@ -149,7 +147,7 @@ func (s *QUICNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) {
 }
 
 func (s *QUICNameServer) newReqID() uint16 {
-	return uint16(atomic.AddUint32(&s.reqID, 1))
+	return 0
 }
 
 func (s *QUICNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) {
diff --git a/infra/conf/merge/rules.go b/infra/conf/merge/rules.go
index 5ba3f909..51ccf2e6 100644
--- a/infra/conf/merge/rules.go
+++ b/infra/conf/merge/rules.go
@@ -4,6 +4,8 @@
 
 package merge
 
+import "fmt"
+
 const (
 	priorityKey string = "_priority"
 	tagKey      string = "_tag"
@@ -11,7 +13,7 @@ const (
 
 // ApplyRules applies merge rules according to _tag, _priority fields, and remove them
 func ApplyRules(m map[string]interface{}) error {
-	err := sortMergeSlices(m)
+	err := sortMergeSlices(m, "")
 	if err != nil {
 		return err
 	}
@@ -20,22 +22,22 @@ func ApplyRules(m map[string]interface{}) error {
 }
 
 // sortMergeSlices enumerates all slices in a map, to sort by priority and merge by tag
-func sortMergeSlices(target map[string]interface{}) error {
+func sortMergeSlices(target map[string]interface{}, path string) error {
 	for key, value := range target {
 		if slice, ok := value.([]interface{}); ok {
 			sortByPriority(slice)
-			s, err := mergeSameTag(slice)
+			s, err := mergeSameTag(slice, fmt.Sprintf("%s.%s", path, key))
 			if err != nil {
 				return err
 			}
 			target[key] = s
 			for _, item := range s {
 				if m, ok := item.(map[string]interface{}); ok {
-					sortMergeSlices(m)
+					sortMergeSlices(m, fmt.Sprintf("%s.%s[]", path, key))
 				}
 			}
 		} else if field, ok := value.(map[string]interface{}); ok {
-			sortMergeSlices(field)
+			sortMergeSlices(field, fmt.Sprintf("%s.%s", path, key))
 		}
 	}
 	return nil
diff --git a/infra/conf/merge/tag.go b/infra/conf/merge/tag.go
index 6c2632e7..3850d551 100644
--- a/infra/conf/merge/tag.go
+++ b/infra/conf/merge/tag.go
@@ -4,10 +4,16 @@
 
 package merge
 
-func getTag(v map[string]interface{}) string {
-	if field, ok := v["tag"]; ok {
-		if t, ok := field.(string); ok {
-			return t
+import (
+	"strings"
+)
+
+func getTag(v map[string]interface{}, tagKeyOnly bool) string {
+	if !tagKeyOnly {
+		if field, ok := v["tag"]; ok {
+			if t, ok := field.(string); ok {
+				return t
+			}
 		}
 	}
 	if field, ok := v[tagKey]; ok {
@@ -18,16 +24,21 @@ func getTag(v map[string]interface{}) string {
 	return ""
 }
 
-func mergeSameTag(s []interface{}) ([]interface{}, error) {
+func mergeSameTag(s []interface{}, path string) ([]interface{}, error) {
 	// from: [a,"",b,"",a,"",b,""]
 	// to: [a,"",b,"",merged,"",merged,""]
 	merged := &struct{}{}
+	tagKeyOnly := false
+	// See https://github.com/v2fly/v2ray-core/issues/2981
+	if strings.HasPrefix(path, ".dns.servers") {
+		tagKeyOnly = true
+	}
 	for i, item1 := range s {
 		map1, ok := item1.(map[string]interface{})
 		if !ok {
 			continue
 		}
-		tag1 := getTag(map1)
+		tag1 := getTag(map1, tagKeyOnly)
 		if tag1 == "" {
 			continue
 		}
@@ -36,7 +47,7 @@ func mergeSameTag(s []interface{}) ([]interface{}, error) {
 			if !ok {
 				continue
 			}
-			tag2 := getTag(map2)
+			tag2 := getTag(map2, tagKeyOnly)
 			if tag1 == tag2 {
 				s[j] = merged
 				err := mergeMaps(map1, map2)
diff --git a/infra/conf/v4/shadowsocks.go b/infra/conf/v4/shadowsocks.go
index ecfc506e..e58458f6 100644
--- a/infra/conf/v4/shadowsocks.go
+++ b/infra/conf/v4/shadowsocks.go
@@ -3,6 +3,7 @@ package v4
 import (
 	"github.com/golang/protobuf/proto"
 
+	"github.com/v2fly/v2ray-core/v5/common/net/packetaddr"
 	"github.com/v2fly/v2ray-core/v5/common/protocol"
 	"github.com/v2fly/v2ray-core/v5/common/serial"
 	"github.com/v2fly/v2ray-core/v5/infra/conf/cfgcommon"
@@ -10,13 +11,14 @@ import (
 )
 
 type ShadowsocksServerConfig struct {
-	Cipher      string                 `json:"method"`
-	Password    string                 `json:"password"`
-	UDP         bool                   `json:"udp"`
-	Level       byte                   `json:"level"`
-	Email       string                 `json:"email"`
-	NetworkList *cfgcommon.NetworkList `json:"network"`
-	IVCheck     bool                   `json:"ivCheck"`
+	Cipher         string                 `json:"method"`
+	Password       string                 `json:"password"`
+	UDP            bool                   `json:"udp"`
+	Level          byte                   `json:"level"`
+	Email          string                 `json:"email"`
+	NetworkList    *cfgcommon.NetworkList `json:"network"`
+	IVCheck        bool                   `json:"ivCheck"`
+	PacketEncoding string                 `json:"packetEncoding"`
 }
 
 func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
@@ -42,6 +44,13 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
 		Account: serial.ToTypedMessage(account),
 	}
 
+	switch v.PacketEncoding {
+	case "Packet":
+		config.PacketEncoding = packetaddr.PacketAddrType_Packet
+	case "", "None":
+		config.PacketEncoding = packetaddr.PacketAddrType_None
+	}
+
 	return config, nil
 }
 
diff --git a/infra/conf/v4/socks.go b/infra/conf/v4/socks.go
index 2a875419..367eb361 100644
--- a/infra/conf/v4/socks.go
+++ b/infra/conf/v4/socks.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/golang/protobuf/proto"
 
+	"github.com/v2fly/v2ray-core/v5/common/net/packetaddr"
 	"github.com/v2fly/v2ray-core/v5/common/protocol"
 	"github.com/v2fly/v2ray-core/v5/common/serial"
 	"github.com/v2fly/v2ray-core/v5/infra/conf/cfgcommon"
@@ -30,12 +31,13 @@ const (
 )
 
 type SocksServerConfig struct {
-	AuthMethod string             `json:"auth"`
-	Accounts   []*SocksAccount    `json:"accounts"`
-	UDP        bool               `json:"udp"`
-	Host       *cfgcommon.Address `json:"ip"`
-	Timeout    uint32             `json:"timeout"`
-	UserLevel  uint32             `json:"userLevel"`
+	AuthMethod     string             `json:"auth"`
+	Accounts       []*SocksAccount    `json:"accounts"`
+	UDP            bool               `json:"udp"`
+	Host           *cfgcommon.Address `json:"ip"`
+	Timeout        uint32             `json:"timeout"`
+	UserLevel      uint32             `json:"userLevel"`
+	PacketEncoding string             `json:"packetEncoding"`
 }
 
 func (v *SocksServerConfig) Build() (proto.Message, error) {
@@ -64,6 +66,14 @@ func (v *SocksServerConfig) Build() (proto.Message, error) {
 
 	config.Timeout = v.Timeout
 	config.UserLevel = v.UserLevel
+
+	switch v.PacketEncoding {
+	case "Packet":
+		config.PacketEncoding = packetaddr.PacketAddrType_Packet
+	case "", "None":
+		config.PacketEncoding = packetaddr.PacketAddrType_None
+	}
+
 	return config, nil
 }
 
diff --git a/infra/conf/v4/trojan.go b/infra/conf/v4/trojan.go
index 56551da2..ac339e80 100644
--- a/infra/conf/v4/trojan.go
+++ b/infra/conf/v4/trojan.go
@@ -11,6 +11,7 @@ import (
 	"github.com/golang/protobuf/proto"
 
 	"github.com/v2fly/v2ray-core/v5/common/net"
+	"github.com/v2fly/v2ray-core/v5/common/net/packetaddr"
 	"github.com/v2fly/v2ray-core/v5/common/protocol"
 	"github.com/v2fly/v2ray-core/v5/common/serial"
 	"github.com/v2fly/v2ray-core/v5/infra/conf/cfgcommon"
@@ -91,9 +92,10 @@ type TrojanUserConfig struct {
 
 // TrojanServerConfig is Inbound configuration
 type TrojanServerConfig struct {
-	Clients   []*TrojanUserConfig      `json:"clients"`
-	Fallback  json.RawMessage          `json:"fallback"`
-	Fallbacks []*TrojanInboundFallback `json:"fallbacks"`
+	Clients        []*TrojanUserConfig      `json:"clients"`
+	Fallback       json.RawMessage          `json:"fallback"`
+	Fallbacks      []*TrojanInboundFallback `json:"fallbacks"`
+	PacketEncoding string                   `json:"packetEncoding"`
 }
 
 // Build implements Buildable
@@ -167,5 +169,12 @@ func (c *TrojanServerConfig) Build() (proto.Message, error) {
 		}
 	}
 
+	switch c.PacketEncoding {
+	case "Packet":
+		config.PacketEncoding = packetaddr.PacketAddrType_Packet
+	case "", "None":
+		config.PacketEncoding = packetaddr.PacketAddrType_None
+	}
+
 	return config, nil
 }
diff --git a/infra/conf/v4/v2ray.go b/infra/conf/v4/v2ray.go
index 399fdc22..9ca662d1 100644
--- a/infra/conf/v4/v2ray.go
+++ b/infra/conf/v4/v2ray.go
@@ -1,7 +1,9 @@
 package v4
 
 import (
+	"context"
 	"encoding/json"
+	"fmt"
 	"path/filepath"
 	"strings"
 
@@ -21,6 +23,7 @@ import (
 	"github.com/v2fly/v2ray-core/v5/infra/conf/synthetic/dns"
 	"github.com/v2fly/v2ray-core/v5/infra/conf/synthetic/log"
 	"github.com/v2fly/v2ray-core/v5/infra/conf/synthetic/router"
+	"github.com/v2fly/v2ray-core/v5/infra/conf/v5cfg"
 )
 
 var (
@@ -481,15 +484,12 @@ func (c *Config) Build() (*core.Config, error) {
 
 	// Load Additional Services that do not have a json translator
 
-	if msg, err := c.BuildServices(c.Services); err != nil {
-		developererr := newError("Loading a V2Ray Features as a service is intended for developers only. " +
-			"This is used for developers to prototype new features or for an advanced client to use special features in V2Ray," +
-			" instead of allowing end user to enable it without special tool and knowledge.")
-		sb := strings.Builder{}
-		return nil, newError("Cannot load service").Base(developererr).Base(err).Base(newError(sb.String()))
-	} else { // nolint: revive
-		// Using a else here is required to keep msg in scope
-		config.App = append(config.App, msg...)
+	for serviceName, service := range c.Services {
+		servicePackedConfig, err := v5cfg.LoadHeterogeneousConfigFromRawJSON(context.Background(), "service", serviceName, *service)
+		if err != nil {
+			return nil, newError(fmt.Sprintf("failed to parse %v config in Services", serviceName)).Base(err)
+		}
+		config.App = append(config.App, serial.ToTypedMessage(servicePackedConfig))
 	}
 
 	var inbounds []InboundDetourConfig
diff --git a/proxy/http/client.go b/proxy/http/client.go
index 316ed74a..36aa0cfa 100644
--- a/proxy/http/client.go
+++ b/proxy/http/client.go
@@ -27,7 +27,7 @@ import (
 	"github.com/v2fly/v2ray-core/v5/proxy"
 	"github.com/v2fly/v2ray-core/v5/transport"
 	"github.com/v2fly/v2ray-core/v5/transport/internet"
-	"github.com/v2fly/v2ray-core/v5/transport/internet/tls"
+	"github.com/v2fly/v2ray-core/v5/transport/internet/security"
 )
 
 type Client struct {
@@ -288,12 +288,12 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u
 	}
 
 	nextProto := ""
-	if tlsConn, ok := iConn.(*tls.Conn); ok {
-		if err := tlsConn.Handshake(); err != nil {
+	if connALPNGetter, ok := iConn.(security.ConnectionApplicationProtocol); ok {
+		nextProto, err = connALPNGetter.GetConnectionApplicationProtocol()
+		if err != nil {
 			rawConn.Close()
 			return nil, nil, err
 		}
-		nextProto = tlsConn.ConnectionState().NegotiatedProtocol
 	}
 
 	switch nextProto {


Test patch:
diff --git a/infra/conf/merge/merge_test.go b/infra/conf/merge/merge_test.go
index 6f393a9e..331d03b7 100644
--- a/infra/conf/merge/merge_test.go
+++ b/infra/conf/merge/merge_test.go
@@ -78,7 +78,7 @@ func TestMergeTag(t *testing.T) {
 				"outboundTag": "out-2"
 			}]
 		}
-	}	
+	}
 `
 	expected := `
 	{
@@ -194,6 +194,108 @@ func TestMergeTagDeep(t *testing.T) {
 	assertResult(t, m, expected)
 }
 
+func TestNoMergeDnsServers(t *testing.T) {
+	json1 := `
+	{
+		"dns": {
+			"queryStrategy": "UseIPv4",
+			"fallbackStrategy": "disabled-if-any-match",
+			"domainMatcher": "mph",
+			"servers": [
+				{
+					"address": "aaa.bbb.ccc.ddd",
+					"port": 53,
+					"domains": [
+						"geosite:cn"
+					],
+					"tag": "dns-domestic"
+				},
+				{
+					"address": "114.114.114.114",
+					"port": 53,
+					"domains": [
+						"geosite:cn"
+					],
+					"tag": "dns-domestic"
+				},
+				{
+					"address": "https://1.1.1.1/dns-query",
+					"tag": "dns-international"
+				}
+			]
+		},
+		"routing": {
+			"domainStrategy": "IPIfNonMatch",
+			"domainMatcher": "mph",
+			"rules": [
+				{
+					"type": "field",
+					"inboundTag": "dns-domestic",
+					"outboundTag": "direct"
+				},
+				{
+					"type": "field",
+					"inboundTag": "dns-international",
+					"outboundTag": "proxy"
+				}
+			]
+		}
+	}
+`
+	expected := `
+	{
+		"dns": {
+			"queryStrategy": "UseIPv4",
+			"fallbackStrategy": "disabled-if-any-match",
+			"domainMatcher": "mph",
+			"servers": [
+				{
+					"address": "aaa.bbb.ccc.ddd",
+					"port": 53,
+					"domains": [
+						"geosite:cn"
+					],
+					"tag": "dns-domestic"
+				},
+				{
+					"address": "114.114.114.114",
+					"port": 53,
+					"domains": [
+						"geosite:cn"
+					],
+					"tag": "dns-domestic"
+				},
+				{
+					"address": "https://1.1.1.1/dns-query",
+					"tag": "dns-international"
+				}
+			]
+		},
+		"routing": {
+			"domainStrategy": "IPIfNonMatch",
+			"domainMatcher": "mph",
+			"rules": [
+				{
+					"type": "field",
+					"inboundTag": "dns-domestic",
+					"outboundTag": "direct"
+				},
+				{
+					"type": "field",
+					"inboundTag": "dns-international",
+					"outboundTag": "proxy"
+				}
+			]
+		}
+	}
+`
+	m, err := merge.JSONs([][]byte{[]byte(json1)})
+	if err != nil {
+		t.Error(err)
+	}
+	assertResult(t, m, expected)
+}
+
 func assertResult(t *testing.T, value []byte, expected string) {
 	v := make(map[string]interface{})
 	err := serial.DecodeJSON(bytes.NewReader(value), &v)
diff --git a/infra/conf/v4/socks_test.go b/infra/conf/v4/socks_test.go
index 1d4a881a..3f6fc429 100644
--- a/infra/conf/v4/socks_test.go
+++ b/infra/conf/v4/socks_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/v2fly/v2ray-core/v5/common/net"
+	"github.com/v2fly/v2ray-core/v5/common/net/packetaddr"
 	"github.com/v2fly/v2ray-core/v5/common/protocol"
 	"github.com/v2fly/v2ray-core/v5/common/serial"
 	"github.com/v2fly/v2ray-core/v5/infra/conf/cfgcommon"
@@ -30,7 +31,8 @@ func TestSocksInboundConfig(t *testing.T) {
 				"udp": false,
 				"ip": "127.0.0.1",
 				"timeout": 5,
-				"userLevel": 1
+				"userLevel": 1,
+				"packetEncoding": "Packet"
 			}`,
 			Parser: testassist.LoadJSON(creator),
 			Output: &socks.ServerConfig{
@@ -44,8 +46,9 @@ func TestSocksInboundConfig(t *testing.T) {
 						Ip: []byte{127, 0, 0, 1},
 					},
 				},
-				Timeout:   5,
-				UserLevel: 1,
+				Timeout:        5,
+				UserLevel:      1,
+				PacketEncoding: packetaddr.PacketAddrType_Packet,
 			},
 		},
 	})