Compare commits

..
Author SHA1 Message Date
Fangliding a0bf239fc1 Add maxReuseTimes 2026-02-18 00:30:51 +08:00
Fangliding dee64ef240 little refactor 2026-02-18 00:30:51 +08:00
Fangliding 0a1b5bfb51 Bench 2026-02-05 20:25:32 +08:00
41 changed files with 306 additions and 314 deletions
-16
View File
@@ -34,22 +34,6 @@ jobs:
if: steps.check-assets.outputs.missing == 'true'
run: sleep 90
check-proto:
runs-on: ubuntu-latest
steps:
- name: Checkout codebase
uses: actions/checkout@v6
- name: Check Proto Version Header
run: |
head -n 4 core/config.pb.go > ref.txt
find . -name "*.pb.go" ! -name "*_grpc.pb.go" -print0 | while IFS= read -r -d '' file; do
if ! cmp -s ref.txt <(head -n 4 "$file"); then
echo "Error: Header mismatch in $file"
head -n 4 "$file"
exit 1
fi
done
test:
needs: check-assets
permissions:
-1
View File
@@ -214,7 +214,6 @@ func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte,
req.Header.Add("Accept", "application/dns-message")
req.Header.Add("Content-Type", "application/dns-message")
req.Header.Set("User-Agent", utils.ChromeUA)
req.Header.Set("X-Padding", utils.H2Base62Pad(crypto.RandBetween(100, 1000)))
hc := s.httpClient
-2
View File
@@ -7,7 +7,6 @@ import (
"time"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport/internet/tagged"
)
@@ -62,7 +61,6 @@ func (s *pingClient) MeasureDelay(httpMethod string) (time.Duration, error) {
if err != nil {
return rttFailed, err
}
req.Header.Set("User-Agent", utils.ChromeUA)
start := time.Now()
resp, err := s.httpClient.Do(req)
+1 -4
View File
@@ -15,7 +15,6 @@ import (
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal/done"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/extension"
"github.com/xtls/xray-core/features/outbound"
@@ -163,9 +162,7 @@ func (o *Observer) probe(outbound string) ProbeResult {
if o.config.ProbeUrl != "" {
probeURL = o.config.ProbeUrl
}
req, _ := http.NewRequest(http.MethodGet, probeURL, nil)
req.Header.Set("User-Agent", utils.ChromeUA)
response, err := httpClient.Do(req)
response, err := httpClient.Get(probeURL)
if err != nil {
return errors.New("outbound failed to relay connection").Base(err)
}
+14 -4
View File
@@ -411,8 +411,10 @@ type MultiplexingConfig struct {
XudpConcurrency int32 `protobuf:"varint,3,opt,name=xudpConcurrency,proto3" json:"xudpConcurrency,omitempty"`
// "reject" (default), "allow" or "skip".
XudpProxyUDP443 string `protobuf:"bytes,4,opt,name=xudpProxyUDP443,proto3" json:"xudpProxyUDP443,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// MaxReuseTimes for an connection
MaxReuseTimes int32 `protobuf:"varint,5,opt,name=maxReuseTimes,proto3" json:"maxReuseTimes,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *MultiplexingConfig) Reset() {
@@ -473,6 +475,13 @@ func (x *MultiplexingConfig) GetXudpProxyUDP443() string {
return ""
}
func (x *MultiplexingConfig) GetMaxReuseTimes() int32 {
if x != nil {
return x.MaxReuseTimes
}
return 0
}
var File_app_proxyman_config_proto protoreflect.FileDescriptor
const file_app_proxyman_config_proto_rawDesc = "" +
@@ -503,12 +512,13 @@ const file_app_proxyman_config_proto_rawDesc = "" +
"\x0eproxy_settings\x18\x03 \x01(\v2$.xray.transport.internet.ProxyConfigR\rproxySettings\x12T\n" +
"\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\n" +
"\bvia_cidr\x18\x05 \x01(\tR\aviaCidr\x12P\n" +
"\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategy\"\xa4\x01\n" +
"\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategy\"\xca\x01\n" +
"\x12MultiplexingConfig\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\x12 \n" +
"\vconcurrency\x18\x02 \x01(\x05R\vconcurrency\x12(\n" +
"\x0fxudpConcurrency\x18\x03 \x01(\x05R\x0fxudpConcurrency\x12(\n" +
"\x0fxudpProxyUDP443\x18\x04 \x01(\tR\x0fxudpProxyUDP443BU\n" +
"\x0fxudpProxyUDP443\x18\x04 \x01(\tR\x0fxudpProxyUDP443\x12$\n" +
"\rmaxReuseTimes\x18\x05 \x01(\x05R\rmaxReuseTimesBU\n" +
"\x15com.xray.app.proxymanP\x01Z&github.com/xtls/xray-core/app/proxyman\xaa\x02\x11Xray.App.Proxymanb\x06proto3"
var (
+2
View File
@@ -68,4 +68,6 @@ message MultiplexingConfig {
int32 xudpConcurrency = 3;
// "reject" (default), "allow" or "skip".
string xudpProxyUDP443 = 4;
// MaxReuseTimes for an connection
int32 maxReuseTimes = 5;
}
+8 -2
View File
@@ -121,6 +121,12 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
if h.senderSettings != nil && h.senderSettings.MultiplexSettings != nil {
if config := h.senderSettings.MultiplexSettings; config.Enabled {
// MaxReuseTimes use 60000 as default, and it also means the upper limit of MaxReuseTimes
// In mux cool spec, connection ID is 2 bytes, so physical limit is 65535, bu we reserve some IDs for future use
MaxReuseTimes := uint32(60000)
if config.MaxReuseTimes != 0 && config.MaxReuseTimes < 60000 {
MaxReuseTimes = uint32(config.MaxReuseTimes)
}
if config.Concurrency < 0 {
h.mux = &mux.ClientManager{Enabled: false}
}
@@ -136,7 +142,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
Dialer: h,
Strategy: mux.ClientStrategy{
MaxConcurrency: uint32(config.Concurrency),
MaxConnection: 128,
MaxReuseTimes: MaxReuseTimes,
},
},
},
@@ -157,7 +163,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
Dialer: h,
Strategy: mux.ClientStrategy{
MaxConcurrency: uint32(config.XudpConcurrency),
MaxConnection: 128,
MaxReuseTimes: 128,
},
},
},
+8
View File
@@ -56,6 +56,10 @@ type readError struct {
error
}
func NewReadError(err error) error {
return readError{err}
}
func (e readError) Error() string {
return e.error.Error()
}
@@ -74,6 +78,10 @@ type writeError struct {
error
}
func NewWriteError(err error) error {
return writeError{err}
}
func (e writeError) Error() string {
return e.error.Error()
}
+70
View File
@@ -0,0 +1,70 @@
package mux_test
import (
"context"
"testing"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/mux"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/pipe"
)
func BenchmarkMuxThroughput(b *testing.B) {
serverCtx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}})
muxServerUplink, muxServerDownlink := newLinkPair()
dispatcher := TestDispatcher{
OnDispatch: func(ctx context.Context, dest net.Destination) (*transport.Link, error) {
inputReader, inputWriter := pipe.New(pipe.WithSizeLimit(512 * 1024))
outputReader, outputWriter := pipe.New(pipe.WithSizeLimit(512 * 1024))
go func() {
defer outputWriter.Close()
for {
mb, err := inputReader.ReadMultiBuffer()
if err != nil {
break
}
buf.ReleaseMulti(mb)
}
}()
return &transport.Link{
Reader: outputReader,
Writer: inputWriter,
}, nil
},
}
_, err := mux.NewServerWorker(serverCtx, &dispatcher, muxServerUplink)
common.Must(err)
client, err := mux.NewClientWorker(*muxServerDownlink, mux.ClientStrategy{})
common.Must(err)
clientCtx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{
Target: net.TCPDestination(net.DomainAddress("www.example.com"), 80),
}})
muxClientUplink, muxClientDownlink := newLinkPair()
go func() {
for {
mb, err := muxClientDownlink.Reader.ReadMultiBuffer()
if err != nil {
break
}
buf.ReleaseMulti(mb)
}
}()
ok := client.Dispatch(clientCtx, muxClientUplink)
if !ok {
b.Fatal("failed to dispatch")
}
data := buf.FromBytes(make([]byte, 8192))
b.SetBytes(int64(8192))
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := muxClientUplink.Writer.WriteMultiBuffer(buf.MultiBuffer{data})
if err != nil {
b.Fatal(err)
}
}
}
+21 -6
View File
@@ -170,7 +170,7 @@ func (f *DialingWorkerFactory) Create() (*ClientWorker, error) {
type ClientStrategy struct {
MaxConcurrency uint32
MaxConnection uint32
MaxReuseTimes uint32
}
type ClientWorker struct {
@@ -179,6 +179,7 @@ type ClientWorker struct {
done *done.Instance
timer *time.Ticker
strategy ClientStrategy
timeCretaed time.Time
}
var (
@@ -194,6 +195,7 @@ func NewClientWorker(stream transport.Link, s ClientStrategy) (*ClientWorker, er
done: done.New(),
timer: time.NewTicker(time.Second * 16),
strategy: s,
timeCretaed: time.Now(),
}
go c.fetchOutput()
@@ -288,7 +290,7 @@ func fetchInput(ctx context.Context, s *Session, output buf.Writer) {
func (m *ClientWorker) IsClosing() bool {
sm := m.sessionManager
if m.strategy.MaxConnection > 0 && sm.Count() >= int(m.strategy.MaxConnection) {
if m.strategy.MaxReuseTimes > 0 && sm.Count() >= int(m.strategy.MaxReuseTimes) {
return true
}
return false
@@ -318,6 +320,7 @@ func (m *ClientWorker) Dispatch(ctx context.Context, link *transport.Link) bool
if s == nil {
return false
}
errors.LogInfo(ctx, "Allocated mux.cool sub connection ID: ", s.ID, "/", m.strategy.MaxReuseTimes, " living: ", m.ActiveConnections(), "/", m.strategy.MaxConcurrency, " age: ", time.Since(m.timeCretaed).Truncate(time.Second))
s.input = link.Reader
s.output = link.Writer
go fetchInput(ctx, s, m.link.Writer)
@@ -332,14 +335,14 @@ func (m *ClientWorker) Dispatch(ctx context.Context, link *transport.Link) bool
func (m *ClientWorker) handleStatueKeepAlive(meta *FrameMetadata, reader *buf.BufferedReader) error {
if meta.Option.Has(OptionData) {
return buf.Copy(NewStreamReader(reader), buf.Discard)
return CopyChunk(reader, buf.Discard)
}
return nil
}
func (m *ClientWorker) handleStatusNew(meta *FrameMetadata, reader *buf.BufferedReader) error {
if meta.Option.Has(OptionData) {
return buf.Copy(NewStreamReader(reader), buf.Discard)
return CopyChunk(reader, buf.Discard)
}
return nil
}
@@ -355,7 +358,19 @@ func (m *ClientWorker) handleStatusKeep(meta *FrameMetadata, reader *buf.Buffere
closingWriter := NewResponseWriter(meta.SessionID, m.link.Writer, protocol.TransferTypeStream)
closingWriter.Close()
return buf.Copy(NewStreamReader(reader), buf.Discard)
return CopyChunk(reader, buf.Discard)
}
if s.transferType == protocol.TransferTypeStream {
err := CopyChunk(reader, s.output)
if err != nil && buf.IsWriteError(err) {
errors.LogInfoInner(context.Background(), err, "failed to write to downstream. closing session ", s.ID)
s.Close(false)
// down stream can have a write err but don't return the err to terminate the whole mux connection
// because it's still available for other sessions
return nil
}
return err
}
rr := s.NewReader(reader, &meta.Target)
@@ -374,7 +389,7 @@ func (m *ClientWorker) handleStatusEnd(meta *FrameMetadata, reader *buf.Buffered
s.Close(false)
}
if meta.Option.Has(OptionData) {
return buf.Copy(NewStreamReader(reader), buf.Discard)
return CopyChunk(reader, buf.Discard)
}
return nil
}
+2 -2
View File
@@ -58,7 +58,7 @@ func TestClientWorkerClose(t *testing.T) {
Writer: w1,
}, mux.ClientStrategy{
MaxConcurrency: 4,
MaxConnection: 4,
MaxReuseTimes: 4,
})
common.Must(err)
@@ -68,7 +68,7 @@ func TestClientWorkerClose(t *testing.T) {
Writer: w2,
}, mux.ClientStrategy{
MaxConcurrency: 4,
MaxConnection: 4,
MaxReuseTimes: 4,
})
common.Must(err)
+29
View File
@@ -57,3 +57,32 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
func NewStreamReader(reader *buf.BufferedReader) buf.Reader {
return crypto.NewChunkStreamReaderWithChunkCount(crypto.PlainChunkSizeParser{}, reader, 1)
}
func CopyChunk(reader *buf.BufferedReader, writer buf.Writer) error {
size, err := serial.ReadUint16(reader)
if err != nil {
return err
}
var writeErr error
for size > 0 {
mb, readErr := reader.ReadAtMost(int32(size))
if !mb.IsEmpty() {
size -= uint16(mb.Len())
if writeErr == nil {
if err := writer.WriteMultiBuffer(mb); err != nil {
writeErr = err
}
} else {
buf.ReleaseMulti(mb)
}
continue
}
if readErr != nil {
return buf.NewReadError(readErr)
}
}
if writeErr != nil {
return buf.NewWriteError(writeErr)
}
return nil
}
+25 -4
View File
@@ -157,7 +157,7 @@ func (w *ServerWorker) Close() error {
func (w *ServerWorker) handleStatusKeepAlive(meta *FrameMetadata, reader *buf.BufferedReader) error {
if meta.Option.Has(OptionData) {
return buf.Copy(NewStreamReader(reader), buf.Discard)
return CopyChunk(reader, buf.Discard)
}
return nil
}
@@ -264,7 +264,7 @@ func (w *ServerWorker) handleStatusNew(ctx context.Context, meta *FrameMetadata,
link, err := w.dispatcher.Dispatch(ctx, meta.Target)
if err != nil {
if meta.Option.Has(OptionData) {
buf.Copy(NewStreamReader(reader), buf.Discard)
CopyChunk(reader, buf.Discard)
}
return errors.New("failed to dispatch request.").Base(err)
}
@@ -287,6 +287,15 @@ func (w *ServerWorker) handleStatusNew(ctx context.Context, meta *FrameMetadata,
return nil
}
if s.transferType == protocol.TransferTypeStream {
err = CopyChunk(reader, s.output)
if err != nil && buf.IsWriteError(err) {
s.Close(false)
return err
}
return err
}
rr := s.NewReader(reader, &meta.Target)
err = buf.Copy(rr, s.output)
@@ -308,7 +317,19 @@ func (w *ServerWorker) handleStatusKeep(meta *FrameMetadata, reader *buf.Buffere
closingWriter := NewResponseWriter(meta.SessionID, w.link.Writer, protocol.TransferTypeStream)
closingWriter.Close()
return buf.Copy(NewStreamReader(reader), buf.Discard)
return CopyChunk(reader, buf.Discard)
}
if s.transferType == protocol.TransferTypeStream {
err := CopyChunk(reader, s.output)
if err != nil && buf.IsWriteError(err) {
errors.LogInfoInner(context.Background(), err, "failed to write to downstream writer. closing session ", s.ID)
s.Close(false)
// down stream can have a write err but don't return the err to terminate the whole mux connection
// because it's still available for other sessions
return nil
}
return err
}
rr := s.NewReader(reader, &meta.Target)
@@ -328,7 +349,7 @@ func (w *ServerWorker) handleStatusEnd(meta *FrameMetadata, reader *buf.Buffered
s.Close(false)
}
if meta.Option.Has(OptionData) {
return buf.Copy(NewStreamReader(reader), buf.Discard)
return CopyChunk(reader, buf.Discard)
}
return nil
}
+1 -1
View File
@@ -15,7 +15,7 @@ import (
)
func newLinkPair() (*transport.Link, *transport.Link) {
opt := pipe.WithoutSizeLimit()
opt := pipe.WithSizeLimit(512 * 1024)
uplinkReader, uplinkWriter := pipe.New(opt)
downlinkReader, downlinkWriter := pipe.New(opt)
+1 -1
View File
@@ -56,7 +56,7 @@ func (m *SessionManager) Allocate(Strategy *ClientStrategy) *Session {
defer m.Unlock()
MaxConcurrency := int(Strategy.MaxConcurrency)
MaxConnection := uint16(Strategy.MaxConnection)
MaxConnection := uint16(Strategy.MaxReuseTimes)
if m.closed || (MaxConcurrency > 0 && len(m.sessions) >= MaxConcurrency) || (MaxConnection > 0 && m.count >= MaxConnection) {
return nil
-17
View File
@@ -1,17 +0,0 @@
package utils
import (
"reflect"
"unsafe"
)
// AccessField can used to access unexported field of a struct
// valueType must be the exact type of the field or it will panic
func AccessField[valueType any](obj any, fieldName string) *valueType {
field := reflect.ValueOf(obj).Elem().FieldByName(fieldName)
if field.Type() != reflect.TypeOf(*new(valueType)) {
panic("field type: " + field.Type().String() + ", valueType: " + reflect.TypeOf(*new(valueType)).String())
}
v := (*valueType)(unsafe.Pointer(field.UnsafeAddr()))
return v
}
-28
View File
@@ -1,28 +0,0 @@
package utils
import (
"math/rand"
"strconv"
"time"
"github.com/klauspost/cpuid/v2"
)
func ChromeVersion() int {
// Use only CPU info as seed for PRNG
seed := int64(cpuid.CPU.Family + cpuid.CPU.Model + cpuid.CPU.PhysicalCores + cpuid.CPU.LogicalCores + cpuid.CPU.CacheLine)
rng := rand.New(rand.NewSource(seed))
// Start from Chrome 144 released on 2026.1.13
releaseDate := time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC)
version := 144
now := time.Now()
// Each version has random 25-45 day interval
for releaseDate.Before(now) {
releaseDate = releaseDate.AddDate(0, 0, rng.Intn(21)+25)
version++
}
return version - 1
}
// ChromeUA provides default browser User-Agent based on CPU-seeded PRNG.
var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(ChromeVersion()) + ".0.0.0 Safari/537.36"
+1 -1
View File
@@ -19,7 +19,7 @@ import (
var (
Version_x byte = 26
Version_y byte = 2
Version_z byte = 6
Version_z byte = 2
)
var (
+2 -2
View File
@@ -1,6 +1,6 @@
module github.com/xtls/xray-core
go 1.25.7
go 1.25.6
require (
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178
@@ -9,7 +9,6 @@ require (
github.com/golang/mock v1.7.0-rc.1
github.com/google/go-cmp v0.7.0
github.com/gorilla/websocket v1.5.3
github.com/klauspost/cpuid/v2 v2.0.12
github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5
github.com/pires/go-proxyproto v0.9.2
@@ -40,6 +39,7 @@ require (
github.com/google/btree v1.1.2 // indirect
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
+5 -3
View File
@@ -4,7 +4,6 @@ import (
"sort"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet/headers/http"
"github.com/xtls/xray-core/transport/internet/headers/noop"
"google.golang.org/protobuf/proto"
@@ -41,8 +40,11 @@ func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
Value: []string{"www.baidu.com", "www.bing.com"},
},
{
Name: "User-Agent",
Value: []string{utils.ChromeUA},
Name: "User-Agent",
Value: []string{
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46",
},
},
{
Name: "Accept-Encoding",
+1 -8
View File
@@ -1,7 +1,6 @@
package conf
import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
@@ -11,7 +10,6 @@ import (
"strconv"
"strings"
"syscall"
"time"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
@@ -749,12 +747,7 @@ func (c *TLSConfig) Build() (proto.Message, error) {
config.MasterKeyLog = c.MasterKeyLog
if c.AllowInsecure {
if time.Now().After(time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)) {
return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"`)
} else {
errors.LogWarning(context.Background(), `"allowInsecure" will be removed automatically after 2026-06-01, please use "pinnedPeerCertSha256"(pcs) and "verifyPeerCertByName"(vcn) instead, PLEASE CONTACT YOUR SERVICE PROVIDER (AIRPORT)`)
config.AllowInsecure = true
}
return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"`)
}
if c.PinnedPeerCertSha256 != "" {
for v := range strings.SplitSeq(c.PinnedPeerCertSha256, ",") {
+2
View File
@@ -101,6 +101,7 @@ func (c *SniffingConfig) Build() (*proxyman.SniffingConfig, error) {
type MuxConfig struct {
Enabled bool `json:"enabled"`
Concurrency int16 `json:"concurrency"`
MaxReuseTimes int32 `json:"maxReuseTimes"`
XudpConcurrency int16 `json:"xudpConcurrency"`
XudpProxyUDP443 string `json:"xudpProxyUDP443"`
}
@@ -117,6 +118,7 @@ func (m *MuxConfig) Build() (*proxyman.MultiplexingConfig, error) {
return &proxyman.MultiplexingConfig{
Enabled: m.Enabled,
Concurrency: int32(m.Concurrency),
MaxReuseTimes: m.MaxReuseTimes,
XudpConcurrency: int32(m.XudpConcurrency),
XudpProxyUDP443: m.XudpProxyUDP443,
}, nil
-78
View File
@@ -1,78 +0,0 @@
package tls
import (
"bytes"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"os"
"text/tabwriter"
"github.com/xtls/xray-core/main/commands/base"
. "github.com/xtls/xray-core/transport/internet/tls"
)
var cmdHash = &base.Command{
UsageLine: "{{.Exec}} tls hash",
Short: "Calculate TLS certificate hash.",
Long: `
xray tls hash --cert <cert.pem>
Calculate TLS certificate hash.
`,
}
func init() {
cmdHash.Run = executeHash // break init loop
}
var input = cmdHash.Flag.String("cert", "fullchain.pem", "The file path of the certificate")
func executeHash(cmd *base.Command, args []string) {
fs := flag.NewFlagSet("hash", flag.ContinueOnError)
if err := fs.Parse(args); err != nil {
fmt.Println(err)
return
}
certContent, err := os.ReadFile(*input)
if err != nil {
fmt.Println(err)
return
}
var certs []*x509.Certificate
if bytes.Contains(certContent, []byte("BEGIN")) {
for {
block, remain := pem.Decode(certContent)
if block == nil {
break
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
fmt.Println("Unable to decode certificate:", err)
return
}
certs = append(certs, cert)
certContent = remain
}
} else {
certs, err = x509.ParseCertificates(certContent)
if err != nil {
fmt.Println("Unable to parse certificates:", err)
return
}
}
if len(certs) == 0 {
fmt.Println("No certificates found")
return
}
tabWriter := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
for i, cert := range certs {
hash := GenerateCertHashHex(cert)
if i == 0 {
fmt.Fprintf(tabWriter, "Leaf SHA256:\t%s\n", hash)
} else {
fmt.Fprintf(tabWriter, "CA <%s> SHA256:\t%s\n", cert.Subject.CommonName, hash)
}
}
tabWriter.Flush()
}
+44
View File
@@ -0,0 +1,44 @@
package tls
import (
"flag"
"fmt"
"os"
"github.com/xtls/xray-core/main/commands/base"
"github.com/xtls/xray-core/transport/internet/tls"
)
var cmdLeafCertHash = &base.Command{
UsageLine: "{{.Exec}} tls leafCertHash",
Short: "Calculate TLS leaf certificate hash.",
Long: `
xray tls leafCertHash --cert <cert.pem>
Calculate TLS leaf certificate hash.
`,
}
func init() {
cmdLeafCertHash.Run = executeLeafCertHash // break init loop
}
var input = cmdLeafCertHash.Flag.String("cert", "fullchain.pem", "The file path of the leaf certificate")
func executeLeafCertHash(cmd *base.Command, args []string) {
fs := flag.NewFlagSet("leafCertHash", flag.ContinueOnError)
if err := fs.Parse(args); err != nil {
fmt.Println(err)
return
}
certContent, err := os.ReadFile(*input)
if err != nil {
fmt.Println(err)
return
}
certChainHashB64, err := tls.CalculatePEMLeafCertSHA256Hash(certContent)
if err != nil {
fmt.Println("failed to decode cert", err)
return
}
fmt.Println(certChainHashB64)
}
+21 -36
View File
@@ -6,13 +6,8 @@ import (
"encoding/hex"
"fmt"
"net"
"os"
"strconv"
"text/tabwriter"
utls "github.com/refraction-networking/utls"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/main/commands/base"
. "github.com/xtls/xray-core/transport/internet/tls"
)
@@ -51,7 +46,6 @@ func executePing(cmd *base.Command, args []string) {
} else {
TargetPort, _ = strconv.Atoi(port)
}
tabWriter := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
var ip net.IP
if len(*pingIPStr) > 0 {
@@ -76,7 +70,7 @@ func executePing(cmd *base.Command, args []string) {
if err != nil {
base.Fatalf("Failed to dial tcp: %s", err)
}
tlsConn := GeneraticUClient(tcpConn, &gotls.Config{
tlsConn := gotls.Client(tcpConn, &gotls.Config{
InsecureSkipVerify: true,
NextProtos: []string{"h2", "http/1.1"},
MaxVersion: gotls.VersionTLS13,
@@ -87,9 +81,8 @@ func executePing(cmd *base.Command, args []string) {
fmt.Println("Handshake failure: ", err)
} else {
fmt.Println("Handshake succeeded")
printTLSConnDetail(tabWriter, tlsConn)
printCertificates(tabWriter, tlsConn.ConnectionState().PeerCertificates)
tabWriter.Flush()
printTLSConnDetail(tlsConn)
printCertificates(tlsConn.ConnectionState().PeerCertificates)
}
tlsConn.Close()
}
@@ -101,7 +94,7 @@ func executePing(cmd *base.Command, args []string) {
if err != nil {
base.Fatalf("Failed to dial tcp: %s", err)
}
tlsConn := GeneraticUClient(tcpConn, &gotls.Config{
tlsConn := gotls.Client(tcpConn, &gotls.Config{
ServerName: domain,
NextProtos: []string{"h2", "http/1.1"},
MaxVersion: gotls.VersionTLS13,
@@ -112,9 +105,8 @@ func executePing(cmd *base.Command, args []string) {
fmt.Println("Handshake failure: ", err)
} else {
fmt.Println("Handshake succeeded")
printTLSConnDetail(tabWriter, tlsConn)
printCertificates(tabWriter, tlsConn.ConnectionState().PeerCertificates)
tabWriter.Flush()
printTLSConnDetail(tlsConn)
printCertificates(tlsConn.ConnectionState().PeerCertificates)
}
tlsConn.Close()
}
@@ -123,45 +115,38 @@ func executePing(cmd *base.Command, args []string) {
fmt.Println("TLS ping finished")
}
func printCertificates(tabWriter *tabwriter.Writer, certs []*x509.Certificate) {
func printCertificates(certs []*x509.Certificate) {
var leaf *x509.Certificate
var CAs []*x509.Certificate
var length int
for _, cert := range certs {
length += len(cert.Raw)
if len(cert.DNSNames) != 0 {
leaf = cert
} else {
CAs = append(CAs, cert)
}
}
fmt.Fprintf(tabWriter, "Certificate chain's total length:\t%d (certs count: %s)\n", length, strconv.Itoa(len(certs)))
fmt.Println("Certificate chain's total length: ", length, "(certs count: "+strconv.Itoa(len(certs))+")")
if leaf != nil {
fmt.Fprintf(tabWriter, "Cert's signature algorithm:\t%s\n", leaf.SignatureAlgorithm.String())
fmt.Fprintf(tabWriter, "Cert's publicKey algorithm:\t%s\n", leaf.PublicKeyAlgorithm.String())
fmt.Fprintf(tabWriter, "Cert's leaf SHA256:\t%s\n", hex.EncodeToString(GenerateCertHash(leaf)))
for _, ca := range CAs {
fmt.Fprintf(tabWriter, "Cert's CA <%s> SHA256:\t%s\n", ca.Subject.CommonName, hex.EncodeToString(GenerateCertHash(ca)))
}
fmt.Fprintf(tabWriter, "Cert's allowed domains:\t%v\n", leaf.DNSNames)
fmt.Println("Cert's signature algorithm: ", leaf.SignatureAlgorithm.String())
fmt.Println("Cert's publicKey algorithm: ", leaf.PublicKeyAlgorithm.String())
fmt.Println("Cert's allowed domains: ", leaf.DNSNames)
fmt.Println("Cert's leaf SHA256: ", hex.EncodeToString(GenerateCertHash(leaf)))
}
}
func printTLSConnDetail(tabWriter *tabwriter.Writer, tlsConn *utls.UConn) {
func printTLSConnDetail(tlsConn *gotls.Conn) {
connectionState := tlsConn.ConnectionState()
var tlsVersion string
switch connectionState.Version {
case gotls.VersionTLS13:
if connectionState.Version == gotls.VersionTLS13 {
tlsVersion = "TLS 1.3"
case gotls.VersionTLS12:
} else if connectionState.Version == gotls.VersionTLS12 {
tlsVersion = "TLS 1.2"
}
fmt.Fprintf(tabWriter, "TLS Version:\t%s\n", tlsVersion)
curveID := utils.AccessField[utls.CurveID](tlsConn.Conn, "curveID")
if curveID != nil {
PostQuantum := (*curveID == utls.X25519MLKEM768)
fmt.Fprintf(tabWriter, "TLS Post-Quantum key exchange:\t%t (%s)\n", PostQuantum, curveID.String())
fmt.Println("TLS Version: ", tlsVersion)
curveID := connectionState.CurveID
if curveID != 0 {
PostQuantum := (curveID == gotls.X25519MLKEM768)
fmt.Println("TLS Post-Quantum key exchange: ", PostQuantum, "("+curveID.String()+")")
} else {
fmt.Fprintf(tabWriter, "TLS Post-Quantum key exchange: false (RSA Exchange)\n")
fmt.Println("TLS Post-Quantum key exchange: false (RSA Exchange)")
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ var CmdTLS = &base.Command{
Commands: []*base.Command{
cmdCert,
cmdPing,
cmdHash,
cmdLeafCertHash,
cmdECH,
},
}
-4
View File
@@ -21,7 +21,6 @@ import (
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/policy"
"github.com/xtls/xray-core/transport"
@@ -220,9 +219,6 @@ func setUpHTTPTunnel(ctx context.Context, dest net.Destination, target string, u
for _, h := range header {
req.Header.Set(h.Key, h.Value)
}
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", utils.ChromeUA)
}
connectHTTP1 := func(rawConn net.Conn) (net.Conn, error) {
req.Header.Set("Proxy-Connection", "Keep-Alive")
+7 -9
View File
@@ -20,7 +20,6 @@ const (
maxPollDelay = 10 * time.Second
pollDelayMultiplier = 2.0
pollLimit = 16
windowSize = 1000
)
type packet struct {
@@ -31,6 +30,7 @@ type packet struct {
type seqStatus struct {
needSeqByte bool
seqByte byte
received bool
}
type xicmpConnClient struct {
@@ -129,14 +129,12 @@ func (c *xicmpConnClient) encode(p []byte) ([]byte, error) {
c.seqStatus[c.seq] = &seqStatus{
needSeqByte: needSeqByte,
seqByte: seqByte,
received: false,
}
delete(c.seqStatus, int(uint16(c.seq-windowSize)))
c.seq++
if c.seq == 65536 {
delete(c.seqStatus, int(uint16(c.seq-windowSize)))
c.seq = 1
}
@@ -170,14 +168,16 @@ func (c *xicmpConnClient) recvLoop() {
continue
}
c.mutex.Lock()
seqStatus, ok := c.seqStatus[echo.Seq]
c.mutex.Unlock()
if !ok {
continue
}
if seqStatus.received {
continue
}
if seqStatus.needSeqByte {
if len(echo.Data) <= 1 {
continue
@@ -189,9 +189,7 @@ func (c *xicmpConnClient) recvLoop() {
}
if len(echo.Data) > 0 {
c.mutex.Lock()
delete(c.seqStatus, echo.Seq)
c.mutex.Unlock()
seqStatus.received = true
buf := make([]byte, len(echo.Data))
copy(buf, echo.Data)
+2 -5
View File
@@ -10,7 +10,6 @@ import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/grpc/encoding"
"github.com/xtls/xray-core/transport/internet/reality"
@@ -168,11 +167,9 @@ func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *in
dialOptions = append(dialOptions, grpc.WithInitialWindowSize(grpcSettings.InitialWindowsSize))
}
userAgent := grpcSettings.UserAgent
if userAgent == "" {
userAgent = utils.ChromeUA
if grpcSettings.UserAgent != "" {
dialOptions = append(dialOptions, grpc.WithUserAgent(grpcSettings.UserAgent))
}
dialOptions = append(dialOptions, grpc.WithUserAgent(userAgent))
var grpcDestHost string
if dest.Address.Family().IsDomain() {
-4
View File
@@ -10,7 +10,6 @@ import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
@@ -87,9 +86,6 @@ func dialhttpUpgrade(ctx context.Context, dest net.Destination, streamSettings *
for key, value := range transportConfiguration.Header {
AddHeader(req.Header, key, value)
}
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", utils.ChromeUA)
}
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "websocket")
+8 -8
View File
@@ -54,15 +54,15 @@ func DialKCP(ctx context.Context, dest net.Destination, streamSettings *internet
return nil, errors.New("failed to dial to dest: ", err).AtWarning().Base(err)
}
wrapper, ok := rawConn.(*internet.PacketConnWrapper)
if !ok {
rawConn.Close()
return nil, errors.New("raw is not PacketConnWrapper")
}
raw := wrapper.Conn
if streamSettings.UdpmaskManager != nil {
wrapper, ok := rawConn.(*internet.PacketConnWrapper)
if !ok {
rawConn.Close()
return nil, errors.New("raw is not PacketConnWrapper")
}
raw := wrapper.Conn
wrapper.Conn, err = streamSettings.UdpmaskManager.WrapPacketConnClient(raw)
if err != nil {
raw.Close()
+1 -2
View File
@@ -27,7 +27,6 @@ import (
"github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/transport/internet/tls"
"golang.org/x/crypto/hkdf"
@@ -223,7 +222,7 @@ func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destinati
if req == nil {
return
}
req.Header.Set("User-Agent", utils.ChromeUA)
req.Header.Set("User-Agent", fingerprint.Client) // TODO: User-Agent map
if first && config.Show {
fmt.Printf("REALITY localAddr: %v\treq.UserAgent(): %v\n", localAddr, req.UserAgent())
}
-4
View File
@@ -6,7 +6,6 @@ import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet"
)
@@ -48,9 +47,6 @@ func (c *Config) GetRequestHeader() http.Header {
for k, v := range c.Headers {
header.Add(k, v)
}
if header.Get("User-Agent") == "" {
header.Set("User-Agent", utils.ChromeUA)
}
return header
}
+5 -8
View File
@@ -289,6 +289,9 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509
if len(certs) == 0 {
return errors.New("unexpected certs")
}
if certs[0].IsCA {
slices.Reverse(certs)
}
// directly return success if pinned cert is leaf
// or replace RootCAs if pinned cert is CA (and can be used in VerifyPeerCertByName)
@@ -381,7 +384,6 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
PinnedPeerCertSha256: c.PinnedPeerCertSha256,
}
config := &tls.Config{
InsecureSkipVerify: c.AllowInsecure,
Rand: randCarrier,
ClientSessionCache: globalSessionCache,
RootCAs: root,
@@ -555,19 +557,14 @@ const (
)
func verifyChain(certs []*x509.Certificate, pinnedPeerCertSha256 [][]byte) (verifyResult, *x509.Certificate) {
leafHash := GenerateCertHash(certs[0])
for _, c := range pinnedPeerCertSha256 {
if hmac.Equal(leafHash, c) {
return foundLeaf, nil
}
}
certs = certs[1:] // skip leaf
for _, cert := range certs {
certHash := GenerateCertHash(cert)
for _, c := range pinnedPeerCertSha256 {
if hmac.Equal(certHash, c) {
if cert.IsCA {
return foundCA, cert
} else {
return foundLeaf, cert
}
}
}
+3 -12
View File
@@ -177,8 +177,7 @@ func (x *Certificate) GetBuildChain() bool {
}
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
AllowInsecure bool `protobuf:"varint,1,opt,name=allow_insecure,json=allowInsecure,proto3" json:"allow_insecure,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
// List of certificates to be served on server.
Certificate []*Certificate `protobuf:"bytes,2,rep,name=certificate,proto3" json:"certificate,omitempty"`
// Override server name.
@@ -242,13 +241,6 @@ func (*Config) Descriptor() ([]byte, []int) {
return file_transport_internet_tls_config_proto_rawDescGZIP(), []int{1}
}
func (x *Config) GetAllowInsecure() bool {
if x != nil {
return x.AllowInsecure
}
return false
}
func (x *Config) GetCertificate() []*Certificate {
if x != nil {
return x.Certificate
@@ -393,9 +385,8 @@ const file_transport_internet_tls_config_proto_rawDesc = "" +
"\x05Usage\x12\x10\n" +
"\fENCIPHERMENT\x10\x00\x12\x14\n" +
"\x10AUTHORITY_VERIFY\x10\x01\x12\x13\n" +
"\x0fAUTHORITY_ISSUE\x10\x02\"\xf5\x06\n" +
"\x06Config\x12%\n" +
"\x0eallow_insecure\x18\x01 \x01(\bR\rallowInsecure\x12J\n" +
"\x0fAUTHORITY_ISSUE\x10\x02\"\xce\x06\n" +
"\x06Config\x12J\n" +
"\vcertificate\x18\x02 \x03(\v2(.xray.transport.internet.tls.CertificateR\vcertificate\x12\x1f\n" +
"\vserver_name\x18\x03 \x01(\tR\n" +
"serverName\x12#\n" +
-2
View File
@@ -38,8 +38,6 @@ message Certificate {
}
message Config {
bool allow_insecure = 1;
// List of certificates to be served on server.
repeated Certificate certificate = 2;
-1
View File
@@ -257,7 +257,6 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b
}
req.Header.Set("Accept", "application/dns-message")
req.Header.Set("Content-Type", "application/dns-message")
req.Header.Set("User-Agent", utils.ChromeUA)
req.Header.Set("X-Padding", utils.H2Base62Pad(crypto.RandBetween(100, 1000)))
resp, err := client.Do(req)
+20 -11
View File
@@ -4,8 +4,28 @@ import (
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/pem"
)
func CalculatePEMLeafCertSHA256Hash(certContent []byte) (string, error) {
var leafCert *x509.Certificate
for {
var err error
block, remain := pem.Decode(certContent)
if block == nil {
break
}
leafCert, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return "", err
}
certContent = remain
}
certHash := GenerateCertHash(leafCert)
certHashHex := hex.EncodeToString(certHash)
return certHashHex, nil
}
// []byte must be ASN.1 DER content
func GenerateCertHash[T *x509.Certificate | []byte](cert T) []byte {
var out [32]byte
@@ -17,14 +37,3 @@ func GenerateCertHash[T *x509.Certificate | []byte](cert T) []byte {
}
return out[:]
}
func GenerateCertHashHex[T *x509.Certificate | []byte](cert T) string {
var out [32]byte
switch v := any(cert).(type) {
case *x509.Certificate:
out = sha256.Sum256(v.Raw)
case []byte:
out = sha256.Sum256(v)
}
return hex.EncodeToString(out[:])
}
-4
View File
@@ -126,10 +126,6 @@ func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) ne
return &UConn{UConn: utlsConn}
}
func GeneraticUClient(c net.Conn, config *tls.Config) *utls.UConn {
return utls.UClient(c, copyConfig(config), utls.HelloChrome_Auto)
}
func copyConfig(c *tls.Config) *utls.Config {
return &utls.Config{
Rand: c.Rand,
+1 -19
View File
@@ -4,7 +4,6 @@ import (
"context"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
@@ -21,24 +20,7 @@ func init() {
if err != nil {
return nil, err
}
if streamSettings != nil && streamSettings.UdpmaskManager != nil {
wrapper, ok := conn.(*internet.PacketConnWrapper)
if !ok {
conn.Close()
return nil, errors.New("conn is not PacketConnWrapper")
}
raw := wrapper.Conn
wrapper.Conn, err = streamSettings.UdpmaskManager.WrapPacketConnClient(raw)
if err != nil {
raw.Close()
return nil, errors.New("mask err").Base(err)
}
}
// TODO: handle dialer options
return conn, nil
return stat.Connection(conn), nil
}))
}
-4
View File
@@ -4,7 +4,6 @@ import (
"net/http"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/utils"
"github.com/xtls/xray-core/transport/internet"
)
@@ -24,9 +23,6 @@ func (c *Config) GetRequestHeader() http.Header {
for k, v := range c.Header {
header.Add(k, v)
}
if header.Get("User-Agent") == "" {
header.Set("User-Agent", utils.ChromeUA)
}
return header
}