Repository: v2fly/v2ray-core (go)

Issue:
transportcommon.ListenWithSecuritySettings listens without security settings
## What version of V2Ray are you using?

v5.16.1


## What's your scenario of using V2Ray?

loopback test

## What problems have you encountered?

On `httpupgrade` and `meek`, `transportcommon.ListenWithSecuritySettings` listens without security settings.

## What's your expectation?

`transportcommon.ListenWithSecuritySettings` listens with security settings.

## Please attach your configuration here

<!-- Remember to mask your IP address or hostname -->

**Server configuration:**

```json
{
	"log": {
		"error": {
			"type": "Console",
			"level": "Debug"
		}
	},
	"inbounds": [
		{
			"listen": "127.0.0.1",
			"port": 1234,
			"protocol": "vmess",
			"settings": {
				"users": [
					"27848739-7e62-4138-9fd3-098a63964b6b"
				]
			},
			"streamSettings": {
				"transport": "httpupgrade",
				"security": "tls",
				"securitySettings": {
					"certificate": [
						{
							"certificateFile": "certificate.crt",
							"keyFile": "private.key"
						}
					]
				}
			}
		}
	],
	"outbounds": [
		{
			"protocol": "freedom"
		}
	]
}
```

**Client configuration:**

```json
{
	"log": {
		"error": {
			"type": "Console",
			"level": "Debug"
		}
	},
	"inbounds": [
		{
			"listen": "127.0.0.1",
			"port": 8080,
			"protocol": "http"
		}
	],
	"outbounds": [
		{
			"protocol": "vmess",
			"settings": {
				"address": "127.0.0.1",
				"port": 1234,
				"uuid": "27848739-7e62-4138-9fd3-098a63964b6b"
			},
			"streamSettings": {
				"transport": "httpupgrade",
				"security": "tls",
				"securitySettings": {
					"serverName": "example.com"
				}
			}
		}
	]
}

```

## Please attach error logs here

<!--
only trailing lines if the log file is large in size.
Error log file is usually at `/var/log/v2ray/error.log` on Linux.
-->

**Server error log:**

```javascript
V2Ray 5.16.1 (V2Fly, a community-driven edition of V2Ray.) Custom (go1.22.2 windows/amd64)
A unified platform for anti-censorship.
2024/06/22 15:32:48 [Debug] app/log: Logger started
2024/06/22 15:32:48 [Debug] app/proxyman/inbound: creating stream worker on 127.0.0.1:1234
2024/06/22 15:32:48 [Info] transport/internet/transportcommon: listening TCP on 127.0.0.1:1234
2024/06/22 15:32:48 [Warning] V2Ray 5.16.1 started
2024/06/22 15:33:24 [Info] transport/internet/httpupgrade: failed to handle request > malformed HTTP request "\x16\x03\x01\x01\x11\x01\x00\x01\r\x03\x03\xe2(\xc7\x06\xe9<yZT\xe2\xb7\xe0)J;\x8ah\x916\xa7E\xde\xe5!\t\xc6эi\xe9\x04\xe7 \x19I\x80\u008a\x13\xf4o\x8eL_\xb9\xc8TP\xf1\xa0Ì\x96\xe2q\xc1\xe6\xa5\xdf\x025]\xb9\f\xd0\x00&\xc0+\xc0/\xc0,\xc00̨̩\xc0\t\xc0\x13\xc0"
2024/06/22 15:33:24 [Info] transport/internet/httpupgrade: failed to handle request > malformed HTTP request "\x16\x03\x01\x01\x11\x01\x00\x01\r\x03\x03\xd9ud\v\xa1\xe6\xbf\xf2\x1e\xbc\xa5\xa7\x8e\xb7\x88\xb0\xb0\xb2\x1f1/\xf1\x93m\x02AsYЫ\x85\x01 S\x1e\xdf\x1a\xbfƫ?f5;\x1c\x0e\xee\x12l\x0e$\x8eTt\xc1\xdd\"\xe03m\xcd%\xb7\xad\x86\x00&\xc0+\xc0/\xc0,\xc00̨̩\xc0\t\xc0\x13\xc0"
2024/06/22 15:33:31 [Debug] app/log: Logger closing
```

**Client error log:**

```javascript
// server issue, not applicable
```

For `httpupgrade`, I have to manually add these to the code to make it work. `meek` should be similar.
```diff
--- a/transport/internet/httpupgrade/hub.go
+++ b/transport/internet/httpupgrade/hub.go
@@ -3,12 +3,14 @@ package httpupgrade
 import (
        "bufio"
        "context"
+       "crypto/tls"
        "net/http"
        "strings"

        "github.com/v2fly/v2ray-core/v5/common"
        "github.com/v2fly/v2ray-core/v5/common/net"
        "github.com/v2fly/v2ray-core/v5/transport/internet"
+       v2tls "github.com/v2fly/v2ray-core/v5/transport/internet/tls"
        "github.com/v2fly/v2ray-core/v5/transport/internet/transportcommon"
 )

@@ -79,6 +81,13 @@ func listenHTTPUpgrade(ctx context.Context, address net.Address, port net.Port,
        if err != nil {
                return nil, newError("failed to listen on ", address, ":", port).Base(err)
        }
+
+       if config := v2tls.ConfigFromStreamSettings(streamSettings); config != nil {
+               if tlsConfig := config.GetTLSConfig(); tlsConfig != nil {
+                       listener = tls.NewListener(listener, tlsConfig)
+               }
+       }
+
        serverInstance.innnerListener = listener
        go serverInstance.keepAccepting()
        return serverInstance, nil
```


Pull request description:
Fix `transportcommon.ListenWithSecuritySettings`
Fix #3055.

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/app/router/config.go b/app/router/config.go
index c1ac9745..5af1ea75 100644
--- a/app/router/config.go
+++ b/app/router/config.go
@@ -144,7 +144,7 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch
 		return &Balancer{
 			selectors: br.OutboundSelector,
 			strategy:  &LeastPingStrategy{config: s},
-			ohm:       ohm,
+			ohm:       ohm, fallbackTag: br.FallbackTag,
 		}, nil
 	case "leastload":
 		i, err := serial.GetInstanceOf(br.StrategySettings)
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 {
diff --git a/transport/internet/transportcommon/listener.go b/transport/internet/transportcommon/listener.go
index 265ba3f0..66b1e366 100644
--- a/transport/internet/transportcommon/listener.go
+++ b/transport/internet/transportcommon/listener.go
@@ -2,12 +2,14 @@ package transportcommon
 
 import (
 	"context"
+	"crypto/tls"
 
 	"github.com/v2fly/v2ray-core/v5/common/environment"
 	"github.com/v2fly/v2ray-core/v5/common/environment/envctx"
 	"github.com/v2fly/v2ray-core/v5/common/net"
 	"github.com/v2fly/v2ray-core/v5/common/session"
 	"github.com/v2fly/v2ray-core/v5/transport/internet"
+	v2tls "github.com/v2fly/v2ray-core/v5/transport/internet/tls"
 )
 
 func ListenWithSecuritySettings(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig) (
@@ -43,5 +45,11 @@ func ListenWithSecuritySettings(ctx context.Context, address net.Address, port n
 	if streamSettings.SocketSettings != nil && streamSettings.SocketSettings.AcceptProxyProtocol {
 		newError("accepting PROXY protocol").AtWarning().WriteToLog(session.ExportIDToError(ctx))
 	}
+
+	if config := v2tls.ConfigFromStreamSettings(streamSettings); config != nil {
+		if tlsConfig := config.GetTLSConfig(); tlsConfig != nil {
+			l = tls.NewListener(l, tlsConfig)
+		}
+	}
 	return l, nil
 }


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,
 			},
 		},
 	})