mirror of
https://github.com/XTLS/Xray-core.git
synced 2025-08-22 17:46:48 +08:00
Compare commits
5 Commits
a02723e63f
...
v1.250803.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bd86732f68 | ||
![]() |
d4f11e6d68 | ||
![]() |
00f3147242 | ||
![]() |
7cbf5b004c | ||
![]() |
87fff12fd9 |
@@ -17,7 +17,7 @@ import (
|
||||
// Manager manages all inbound handlers.
|
||||
type Manager struct {
|
||||
access sync.RWMutex
|
||||
untaggedHandler []inbound.Handler
|
||||
untaggedHandlers []inbound.Handler
|
||||
taggedHandlers map[string]inbound.Handler
|
||||
running bool
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func (m *Manager) AddHandler(ctx context.Context, handler inbound.Handler) error
|
||||
}
|
||||
m.taggedHandlers[tag] = handler
|
||||
} else {
|
||||
m.untaggedHandler = append(m.untaggedHandler, handler)
|
||||
m.untaggedHandlers = append(m.untaggedHandlers, handler)
|
||||
}
|
||||
|
||||
if m.running {
|
||||
@@ -94,8 +94,8 @@ func (m *Manager) ListHandlers(ctx context.Context) []inbound.Handler {
|
||||
m.access.RLock()
|
||||
defer m.access.RUnlock()
|
||||
|
||||
var response []inbound.Handler
|
||||
copy(m.untaggedHandler, response)
|
||||
response := make([]inbound.Handler, len(m.untaggedHandlers))
|
||||
copy(response, m.untaggedHandlers)
|
||||
|
||||
for _, v := range m.taggedHandlers {
|
||||
response = append(response, v)
|
||||
@@ -117,7 +117,7 @@ func (m *Manager) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, handler := range m.untaggedHandler {
|
||||
for _, handler := range m.untaggedHandlers {
|
||||
if err := handler.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func (m *Manager) Close() error {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
for _, handler := range m.untaggedHandler {
|
||||
for _, handler := range m.untaggedHandlers {
|
||||
if err := handler.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
@@ -150,8 +150,8 @@ func (m *Manager) ListHandlers(ctx context.Context) []outbound.Handler {
|
||||
m.access.RLock()
|
||||
defer m.access.RUnlock()
|
||||
|
||||
var response []outbound.Handler
|
||||
copy(m.untaggedHandlers, response)
|
||||
response := make([]outbound.Handler, len(m.untaggedHandlers))
|
||||
copy(response, m.untaggedHandlers)
|
||||
|
||||
for _, v := range m.taggedHandler {
|
||||
response = append(response, v)
|
||||
|
152
app/version/config.pb.go
Normal file
152
app/version/config.pb.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.35.1
|
||||
// protoc v5.28.2
|
||||
// source: app/version/config.proto
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
CoreVersion string `protobuf:"bytes,1,opt,name=core_version,json=coreVersion,proto3" json:"core_version,omitempty"`
|
||||
MinVersion string `protobuf:"bytes,2,opt,name=min_version,json=minVersion,proto3" json:"min_version,omitempty"`
|
||||
MaxVersion string `protobuf:"bytes,3,opt,name=max_version,json=maxVersion,proto3" json:"max_version,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
mi := &file_app_version_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Config) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Config) ProtoMessage() {}
|
||||
|
||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_app_version_config_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||
func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_app_version_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Config) GetCoreVersion() string {
|
||||
if x != nil {
|
||||
return x.CoreVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetMinVersion() string {
|
||||
if x != nil {
|
||||
return x.MinVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetMaxVersion() string {
|
||||
if x != nil {
|
||||
return x.MaxVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_app_version_config_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_app_version_config_proto_rawDesc = []byte{
|
||||
0x0a, 0x18, 0x61, 0x70, 0x70, 0x2f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x2f, 0x63, 0x6f,
|
||||
0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x78, 0x72, 0x61, 0x79,
|
||||
0x2e, 0x61, 0x70, 0x70, 0x2e, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x6d, 0x0a, 0x06,
|
||||
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x72, 0x65, 0x5f, 0x76,
|
||||
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f,
|
||||
0x72, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x69, 0x6e,
|
||||
0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
|
||||
0x6d, 0x69, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61,
|
||||
0x78, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0a, 0x6d, 0x61, 0x78, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x52, 0x0a, 0x14, 0x63,
|
||||
0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x76, 0x65, 0x72, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x50, 0x01, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
|
||||
0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65,
|
||||
0x2f, 0x61, 0x70, 0x70, 0x2f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0xaa, 0x02, 0x10, 0x58,
|
||||
0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_app_version_config_proto_rawDescOnce sync.Once
|
||||
file_app_version_config_proto_rawDescData = file_app_version_config_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_app_version_config_proto_rawDescGZIP() []byte {
|
||||
file_app_version_config_proto_rawDescOnce.Do(func() {
|
||||
file_app_version_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_version_config_proto_rawDescData)
|
||||
})
|
||||
return file_app_version_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_app_version_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_app_version_config_proto_goTypes = []any{
|
||||
(*Config)(nil), // 0: xray.app.version.Config
|
||||
}
|
||||
var file_app_version_config_proto_depIdxs = []int32{
|
||||
0, // [0:0] is the sub-list for method output_type
|
||||
0, // [0:0] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_app_version_config_proto_init() }
|
||||
func file_app_version_config_proto_init() {
|
||||
if File_app_version_config_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_app_version_config_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_app_version_config_proto_goTypes,
|
||||
DependencyIndexes: file_app_version_config_proto_depIdxs,
|
||||
MessageInfos: file_app_version_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_app_version_config_proto = out.File
|
||||
file_app_version_config_proto_rawDesc = nil
|
||||
file_app_version_config_proto_goTypes = nil
|
||||
file_app_version_config_proto_depIdxs = nil
|
||||
}
|
14
app/version/config.proto
Normal file
14
app/version/config.proto
Normal file
@@ -0,0 +1,14 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package xray.app.version;
|
||||
option csharp_namespace = "Xray.App.Version";
|
||||
option go_package = "github.com/xtls/xray-core/app/version";
|
||||
option java_package = "com.xray.app.version";
|
||||
option java_multiple_files = true;
|
||||
|
||||
|
||||
message Config {
|
||||
string core_version = 1;
|
||||
string min_version = 2;
|
||||
string max_version = 3;
|
||||
}
|
77
app/version/version.go
Normal file
77
app/version/version.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Version struct {
|
||||
config *Config
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func New(ctx context.Context, config *Config) (*Version, error) {
|
||||
if config.MinVersion != "" {
|
||||
result, err := compareVersions(config.MinVersion, config.CoreVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result > 0 {
|
||||
return nil, errors.New("this config must be run on version ", config.MinVersion, " or higher")
|
||||
}
|
||||
}
|
||||
if config.MaxVersion != "" {
|
||||
result, err := compareVersions(config.MaxVersion, config.CoreVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result < 0 {
|
||||
return nil, errors.New("this config should be run on version ", config.MaxVersion, " or lower")
|
||||
}
|
||||
}
|
||||
return &Version{config: config, ctx: ctx}, nil
|
||||
}
|
||||
|
||||
func compareVersions(v1, v2 string) (int, error) {
|
||||
// Split version strings into components
|
||||
v1Parts := strings.Split(v1, ".")
|
||||
v2Parts := strings.Split(v2, ".")
|
||||
|
||||
// Pad shorter versions with zeros
|
||||
for len(v1Parts) < len(v2Parts) {
|
||||
v1Parts = append(v1Parts, "0")
|
||||
}
|
||||
for len(v2Parts) < len(v1Parts) {
|
||||
v2Parts = append(v2Parts, "0")
|
||||
}
|
||||
|
||||
// Compare each part
|
||||
for i := 0; i < len(v1Parts); i++ {
|
||||
// Convert parts to integers
|
||||
n1, err := strconv.Atoi(v1Parts[i])
|
||||
if err != nil {
|
||||
return 0, errors.New("invalid version component ", v1Parts[i], " in ", v1)
|
||||
}
|
||||
n2, err := strconv.Atoi(v2Parts[i])
|
||||
if err != nil {
|
||||
return 0, errors.New("invalid version component ", v2Parts[i], " in ", v2)
|
||||
}
|
||||
|
||||
if n1 < n2 {
|
||||
return -1, nil // v1 < v2
|
||||
}
|
||||
if n1 > n2 {
|
||||
return 1, nil // v1 > v2
|
||||
}
|
||||
}
|
||||
return 0, nil // v1 == v2
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
return New(ctx, config.(*Config))
|
||||
}))
|
||||
}
|
@@ -18,8 +18,8 @@ import (
|
||||
|
||||
var (
|
||||
Version_x byte = 25
|
||||
Version_y byte = 7
|
||||
Version_z byte = 26
|
||||
Version_y byte = 8
|
||||
Version_z byte = 3
|
||||
)
|
||||
|
||||
var (
|
||||
|
@@ -414,7 +414,7 @@ type TLSConfig struct {
|
||||
VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"`
|
||||
ECHServerKeys string `json:"echServerKeys"`
|
||||
ECHConfigList string `json:"echConfigList"`
|
||||
ECHForceQuery bool `json:"echForceQuery"`
|
||||
ECHForceQuery string `json:"echForceQuery"`
|
||||
ECHSocketSettings *SocketConfig `json:"echSockopt"`
|
||||
}
|
||||
|
||||
@@ -494,6 +494,12 @@ func (c *TLSConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
config.EchServerKeys = EchPrivateKey
|
||||
}
|
||||
switch c.ECHForceQuery {
|
||||
case "none", "half", "full", "":
|
||||
config.EchForceQuery = c.ECHForceQuery
|
||||
default:
|
||||
return nil, errors.New(`invalid "echForceQuery": `, c.ECHForceQuery)
|
||||
}
|
||||
config.EchForceQuery = c.ECHForceQuery
|
||||
config.EchConfigList = c.ECHConfigList
|
||||
if c.ECHSocketSettings != nil {
|
||||
|
22
infra/conf/version.go
Normal file
22
infra/conf/version.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/app/version"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type VersionConfig struct {
|
||||
MinVersion string `json:"min"`
|
||||
MaxVersion string `json:"max"`
|
||||
}
|
||||
|
||||
func (c *VersionConfig) Build() (*version.Config, error) {
|
||||
coreVersion := strconv.Itoa(int(core.Version_x)) + "." + strconv.Itoa(int(core.Version_y)) + "." + strconv.Itoa(int(core.Version_z))
|
||||
|
||||
return &version.Config{
|
||||
CoreVersion: coreVersion,
|
||||
MinVersion: c.MinVersion,
|
||||
MaxVersion: c.MaxVersion,
|
||||
}, nil
|
||||
}
|
@@ -383,6 +383,7 @@ type Config struct {
|
||||
FakeDNS *FakeDNSConfig `json:"fakeDns"`
|
||||
Observatory *ObservatoryConfig `json:"observatory"`
|
||||
BurstObservatory *BurstObservatoryConfig `json:"burstObservatory"`
|
||||
Version *VersionConfig `json:"version"`
|
||||
}
|
||||
|
||||
func (c *Config) findInboundTag(tag string) int {
|
||||
@@ -451,6 +452,10 @@ func (c *Config) Override(o *Config, fn string) {
|
||||
c.BurstObservatory = o.BurstObservatory
|
||||
}
|
||||
|
||||
if o.Version != nil {
|
||||
c.Version = o.Version
|
||||
}
|
||||
|
||||
// update the Inbound in slice if the only one in override config has same tag
|
||||
if len(o.InboundConfigs) > 0 {
|
||||
for i := range o.InboundConfigs {
|
||||
@@ -591,6 +596,14 @@ func (c *Config) Build() (*core.Config, error) {
|
||||
config.App = append(config.App, serial.ToTypedMessage(r))
|
||||
}
|
||||
|
||||
if c.Version != nil {
|
||||
r, err := c.Version.Build()
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to build version configuration").Base(err)
|
||||
}
|
||||
config.App = append(config.App, serial.ToTypedMessage(r))
|
||||
}
|
||||
|
||||
var inbounds []InboundDetourConfig
|
||||
|
||||
if len(c.InboundConfigs) > 0 {
|
||||
|
@@ -42,6 +42,9 @@ func ListenTCP(ctx context.Context, address net.Address, port net.Port, streamSe
|
||||
var listener net.Listener
|
||||
var err error
|
||||
if port == net.Port(0) { // unix
|
||||
if !address.Family().IsDomain() {
|
||||
return nil, errors.New("invalid unix listen: ", address).AtError()
|
||||
}
|
||||
listener, err = internet.ListenSystem(ctx, &net.UnixAddr{
|
||||
Name: address.Domain(),
|
||||
Net: "unix",
|
||||
|
@@ -8,7 +8,6 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"github.com/xtls/xray-core/features/dns"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -451,7 +450,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
|
||||
if len(c.EchConfigList) > 0 || len(c.EchServerKeys) > 0 {
|
||||
err := ApplyECH(c, config)
|
||||
if err != nil {
|
||||
if c.EchForceQuery || errors.Cause(err) != dns.ErrEmptyResponse {
|
||||
if c.EchForceQuery == "full" {
|
||||
errors.LogError(context.Background(), err)
|
||||
} else {
|
||||
errors.LogInfo(context.Background(), err)
|
||||
|
@@ -220,7 +220,7 @@ type Config struct {
|
||||
VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"`
|
||||
EchServerKeys []byte `protobuf:"bytes,18,opt,name=ech_server_keys,json=echServerKeys,proto3" json:"ech_server_keys,omitempty"`
|
||||
EchConfigList string `protobuf:"bytes,19,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"`
|
||||
EchForceQuery bool `protobuf:"varint,20,opt,name=ech_force_query,json=echForceQuery,proto3" json:"ech_force_query,omitempty"`
|
||||
EchForceQuery string `protobuf:"bytes,20,opt,name=ech_force_query,json=echForceQuery,proto3" json:"ech_force_query,omitempty"`
|
||||
EchSocketSettings *internet.SocketConfig `protobuf:"bytes,21,opt,name=ech_socket_settings,json=echSocketSettings,proto3" json:"ech_socket_settings,omitempty"`
|
||||
}
|
||||
|
||||
@@ -380,11 +380,11 @@ func (x *Config) GetEchConfigList() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetEchForceQuery() bool {
|
||||
func (x *Config) GetEchForceQuery() string {
|
||||
if x != nil {
|
||||
return x.EchForceQuery
|
||||
}
|
||||
return false
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetEchSocketSettings() *internet.SocketConfig {
|
||||
@@ -483,7 +483,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
|
||||
0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x6c, 0x69, 0x73,
|
||||
0x74, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66,
|
||||
0x69, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x66, 0x6f,
|
||||
0x72, 0x63, 0x65, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52,
|
||||
0x72, 0x63, 0x65, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0d, 0x65, 0x63, 0x68, 0x46, 0x6f, 0x72, 0x63, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x55,
|
||||
0x0a, 0x13, 0x65, 0x63, 0x68, 0x5f, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x73, 0x65, 0x74,
|
||||
0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x78, 0x72,
|
||||
|
@@ -98,7 +98,7 @@ message Config {
|
||||
|
||||
string ech_config_list = 19;
|
||||
|
||||
bool ech_force_query = 20;
|
||||
string ech_force_query = 20;
|
||||
|
||||
SocketConfig ech_socket_settings = 21;
|
||||
}
|
||||
|
@@ -9,10 +9,6 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"github.com/xtls/xray-core/common/crypto"
|
||||
dns2 "github.com/xtls/xray-core/features/dns"
|
||||
"golang.org/x/net/http2"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -21,6 +17,11 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"github.com/xtls/xray-core/common/crypto"
|
||||
dns2 "github.com/xtls/xray-core/features/dns"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/xtls/reality"
|
||||
"github.com/xtls/reality/hpke"
|
||||
@@ -52,10 +53,18 @@ func ApplyECH(c *Config, config *tls.Config) error {
|
||||
|
||||
// for client
|
||||
if len(c.EchConfigList) != 0 {
|
||||
ECHForceQuery := c.EchForceQuery
|
||||
switch ECHForceQuery {
|
||||
case "none", "half", "full":
|
||||
case "":
|
||||
ECHForceQuery = "none" // default to none
|
||||
default:
|
||||
panic("Invalid ECHForceQuery: " + c.EchForceQuery)
|
||||
}
|
||||
defer func() {
|
||||
// if failed to get ECHConfig, use an invalid one to make connection fail
|
||||
if err != nil {
|
||||
if c.EchForceQuery {
|
||||
if err != nil || len(ECHConfig) == 0 {
|
||||
if ECHForceQuery == "full" {
|
||||
ECHConfig = []byte{1, 1, 4, 5, 1, 4}
|
||||
}
|
||||
}
|
||||
@@ -106,32 +115,40 @@ type echConfigRecord struct {
|
||||
}
|
||||
|
||||
var (
|
||||
// key value must be like this: "example.com|udp://1.1.1.1"
|
||||
// The keys for both maps must be generated by ECHCacheKey().
|
||||
GlobalECHConfigCache = utils.NewTypedSyncMap[string, *ECHConfigCache]()
|
||||
clientForECHDOH = utils.NewTypedSyncMap[string, *http.Client]()
|
||||
)
|
||||
|
||||
// sockopt can be nil if not specified.
|
||||
// if for clientForECHDOH, domain can be empty.
|
||||
func ECHCacheKey(server, domain string, sockopt *internet.SocketConfig) string {
|
||||
return server + "|" + domain + "|" + fmt.Sprintf("%p", sockopt)
|
||||
}
|
||||
|
||||
// Update updates the ECH config for given domain and server.
|
||||
// this method is concurrent safe, only one update request will be sent, others get the cache.
|
||||
// if isLockedUpdate is true, it will not try to acquire the lock.
|
||||
func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate bool, forceQuery bool, sockopt *internet.SocketConfig) ([]byte, error) {
|
||||
func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate bool, forceQuery string, sockopt *internet.SocketConfig) ([]byte, error) {
|
||||
if !isLockedUpdate {
|
||||
c.UpdateLock.Lock()
|
||||
defer c.UpdateLock.Unlock()
|
||||
}
|
||||
// Double check cache after acquiring lock
|
||||
configRecord := c.configRecord.Load()
|
||||
if configRecord.expire.After(time.Now()) {
|
||||
if configRecord.expire.After(time.Now()) && configRecord.err == nil {
|
||||
errors.LogDebug(context.Background(), "Cache hit for domain after double check: ", domain)
|
||||
return configRecord.config, configRecord.err
|
||||
}
|
||||
// Query ECH config from DNS server
|
||||
errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server)
|
||||
echConfig, ttl, err := dnsQuery(server, domain, sockopt)
|
||||
if err != nil {
|
||||
if forceQuery || ttl == 0 {
|
||||
return nil, err
|
||||
}
|
||||
// if in "full", directly return
|
||||
if err != nil && forceQuery == "full" {
|
||||
return nil, err
|
||||
}
|
||||
if ttl == 0 {
|
||||
ttl = dns2.DefaultTTL
|
||||
}
|
||||
configRecord = &echConfigRecord{
|
||||
config: echConfig,
|
||||
@@ -144,8 +161,8 @@ func (c *ECHConfigCache) Update(domain string, server string, isLockedUpdate boo
|
||||
|
||||
// QueryRecord returns the ECH config for given domain.
|
||||
// If the record is not in cache or expired, it will query the DNS server and update the cache.
|
||||
func QueryRecord(domain string, server string, forceQuery bool, sockopt *internet.SocketConfig) ([]byte, error) {
|
||||
GlobalECHConfigCacheKey := domain + "|" + server + "|" + fmt.Sprintf("%p", sockopt)
|
||||
func QueryRecord(domain string, server string, forceQuery string, sockopt *internet.SocketConfig) ([]byte, error) {
|
||||
GlobalECHConfigCacheKey := ECHCacheKey(server, domain, sockopt)
|
||||
echConfigCache, ok := GlobalECHConfigCache.Load(GlobalECHConfigCacheKey)
|
||||
if !ok {
|
||||
echConfigCache = &ECHConfigCache{}
|
||||
@@ -153,7 +170,7 @@ func QueryRecord(domain string, server string, forceQuery bool, sockopt *interne
|
||||
echConfigCache, _ = GlobalECHConfigCache.LoadOrStore(GlobalECHConfigCacheKey, echConfigCache)
|
||||
}
|
||||
configRecord := echConfigCache.configRecord.Load()
|
||||
if configRecord.expire.After(time.Now()) {
|
||||
if configRecord.expire.After(time.Now()) && (configRecord.err == nil || forceQuery == "none") {
|
||||
errors.LogDebug(context.Background(), "Cache hit for domain: ", domain)
|
||||
return configRecord.config, configRecord.err
|
||||
}
|
||||
@@ -196,7 +213,7 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b
|
||||
return nil, 0, err
|
||||
}
|
||||
var client *http.Client
|
||||
serverKey := server + "|" + fmt.Sprintf("%p", sockopt)
|
||||
serverKey := ECHCacheKey(server, "", sockopt)
|
||||
if client, _ = clientForECHDOH.Load(serverKey); client == nil {
|
||||
// All traffic sent by core should via xray's internet.DialSystem
|
||||
// This involves the behavior of some Android VPN GUI clients
|
||||
@@ -307,7 +324,8 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, dns2.DefaultTTL, dns2.ErrEmptyResponse
|
||||
// empty is valid, means no ECH config found
|
||||
return nil, dns2.DefaultTTL, nil
|
||||
}
|
||||
|
||||
// reference github.com/OmarTariq612/goech
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -41,7 +40,7 @@ func TestECHDial(t *testing.T) {
|
||||
}
|
||||
wg.Wait()
|
||||
// check cache
|
||||
echConfigCache, ok := GlobalECHConfigCache.Load("encryptedsni.com|udp://1.1.1.1" + "|" + fmt.Sprintf("%p", config.EchSocketSettings))
|
||||
echConfigCache, ok := GlobalECHConfigCache.Load(ECHCacheKey("udp://1.1.1.1", "encryptedsni.com", nil))
|
||||
if !ok {
|
||||
t.Error("ECH config cache not found")
|
||||
|
||||
@@ -60,22 +59,12 @@ func TestECHDial(t *testing.T) {
|
||||
func TestECHDialFail(t *testing.T) {
|
||||
config := &Config{
|
||||
ServerName: "cloudflare.com",
|
||||
EchConfigList: "udp://1.1.1.1",
|
||||
EchConfigList: "udp://127.0.0.1",
|
||||
EchForceQuery: "half",
|
||||
}
|
||||
TLSConfig := config.GetTLSConfig()
|
||||
TLSConfig.NextProtos = []string{"http/1.1"}
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: TLSConfig,
|
||||
},
|
||||
}
|
||||
resp, err := client.Get("https://cloudflare.com/cdn-cgi/trace")
|
||||
common.Must(err)
|
||||
defer resp.Body.Close()
|
||||
_, err = io.ReadAll(resp.Body)
|
||||
common.Must(err)
|
||||
config.GetTLSConfig()
|
||||
// check cache
|
||||
echConfigCache, ok := GlobalECHConfigCache.Load("cloudflare.com|udp://1.1.1.1" + "|" + fmt.Sprintf("%p", config.EchSocketSettings))
|
||||
echConfigCache, ok := GlobalECHConfigCache.Load(ECHCacheKey("udp://127.0.0.1", "cloudflare.com", nil))
|
||||
if !ok {
|
||||
t.Error("ECH config cache not found")
|
||||
}
|
||||
|
Reference in New Issue
Block a user