Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] 1ca08845e3 Address code review: add constants and improve documentation
Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-02-04 22:14:53 +00:00
copilot-swe-agent[bot] ee6f422148 Use internal uuid package instead of external google/uuid
Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-02-04 22:13:04 +00:00
copilot-swe-agent[bot] 8898a24b23 Add local filesystem-based XDRIVE implementation
Co-authored-by: RPRX <63339210+RPRX@users.noreply.github.com>
2026-02-04 22:11:51 +00:00
copilot-swe-agent[bot] 45b6ffccf9 Initial plan 2026-02-04 22:05:27 +00:00
64 changed files with 1216 additions and 1864 deletions
+2 -1
View File
@@ -29,5 +29,6 @@
"openbsd-arm7": { "friendlyName": "openbsd-arm32-v7a" },
"windows-386": { "friendlyName": "windows-32" },
"windows-amd64": { "friendlyName": "windows-64" },
"windows-arm64": { "friendlyName": "windows-arm64-v8a" }
"windows-arm64": { "friendlyName": "windows-arm64-v8a" },
"windows-arm7": { "friendlyName": "windows-arm32-v7a" }
}
+7 -1
View File
@@ -40,7 +40,7 @@ jobs:
break
fi
done
LIST=('amd64' 'x86' 'arm64')
LIST=('amd64' 'x86' 'arm64' 'arm')
for ARCHITECTURE in "${LIST[@]}"
do
echo -e "Checking wintun.dll for ${ARCHITECTURE}..."
@@ -114,6 +114,9 @@ jobs:
# Windows ARM
- goos: windows
goarch: arm64
- goos: windows
goarch: arm
goarm: 7
# BEGIN Other architectures
# BEGIN riscv64 & ARM64 & LOONG64
- goos: linux
@@ -247,6 +250,9 @@ jobs:
if [[ ${GOARCH} == 'arm64' ]]; then
mv resources/wintun/bin/arm64/wintun.dll build_assets/
fi
if [[ ${GOARCH} == 'arm' ]]; then
mv resources/wintun/bin/arm/wintun.dll build_assets/
fi
mv resources/wintun/LICENSE.txt build_assets/LICENSE-wintun.txt
fi
@@ -89,7 +89,7 @@ jobs:
max_attempts: 60
command: |
[ -d 'resources' ] || mkdir resources
LIST=('amd64' 'x86' 'arm64')
LIST=('amd64' 'x86' 'arm64' 'arm')
for ARCHITECTURE in "${LIST[@]}"
do
FILE_PATH="resources/wintun/bin/${ARCHITECTURE}/wintun.dll"
-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)
}
+2 -11
View File
@@ -411,7 +411,6 @@ 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"`
BrutalBPS uint64 `protobuf:"varint,5,opt,name=brutalBPS,proto3" json:"brutalBPS,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -474,13 +473,6 @@ func (x *MultiplexingConfig) GetXudpProxyUDP443() string {
return ""
}
func (x *MultiplexingConfig) GetBrutalBPS() uint64 {
if x != nil {
return x.BrutalBPS
}
return 0
}
var File_app_proxyman_config_proto protoreflect.FileDescriptor
const file_app_proxyman_config_proto_rawDesc = "" +
@@ -511,13 +503,12 @@ 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\"\xc2\x01\n" +
"\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategy\"\xa4\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\x0fxudpProxyUDP443\x12\x1c\n" +
"\tbrutalBPS\x18\x05 \x01(\x04R\tbrutalBPSBU\n" +
"\x0fxudpProxyUDP443\x18\x04 \x01(\tR\x0fxudpProxyUDP443BU\n" +
"\x15com.xray.app.proxymanP\x01Z&github.com/xtls/xray-core/app/proxyman\xaa\x02\x11Xray.App.Proxymanb\x06proto3"
var (
-1
View File
@@ -68,5 +68,4 @@ message MultiplexingConfig {
int32 xudpConcurrency = 3;
// "reject" (default), "allow" or "skip".
string xudpProxyUDP443 = 4;
uint64 brutalBPS = 5;
}
-9
View File
@@ -19,8 +19,6 @@ import (
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/features/stats"
"github.com/xtls/xray-core/proxy"
"github.com/xtls/xray-core/proxy/hysteria/account"
hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tcp"
@@ -140,13 +138,6 @@ func (w *tcpWorker) Proxy() proxy.Inbound {
func (w *tcpWorker) Start() error {
ctx := context.Background()
type HysteriaInboundValidator interface{ HysteriaInboundValidator() *account.Validator }
if v, ok := w.proxy.(HysteriaInboundValidator); ok {
ctx = hyCtx.ContextWithRequireDatagram(ctx, true)
ctx = hyCtx.ContextWithValidator(ctx, v.HysteriaInboundValidator())
}
hub, err := internet.ListenTCP(ctx, w.address, w.port, w.stream, func(conn stat.Connection) {
go w.callback(conn)
})
+2 -4
View File
@@ -136,8 +136,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
Dialer: h,
Strategy: mux.ClientStrategy{
MaxConcurrency: uint32(config.Concurrency),
MaxReuseTimes: 32768,
BrutalBPS: config.BrutalBPS,
MaxConnection: 128,
},
},
},
@@ -158,8 +157,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
Dialer: h,
Strategy: mux.ClientStrategy{
MaxConcurrency: uint32(config.XudpConcurrency),
MaxReuseTimes: 32768,
BrutalBPS: config.BrutalBPS,
MaxConnection: 128,
},
},
},
-6
View File
@@ -75,12 +75,6 @@ func (mb MultiBuffer) Copy(b []byte) int {
return total
}
func (mb MultiBuffer) Bytes() []byte {
b := make([]byte, mb.Len())
mb.Copy(b)
return b
}
// ReadFrom reads all content from reader until EOF.
func ReadFrom(reader io.Reader) (MultiBuffer, error) {
mb := make(MultiBuffer, 0, 16)
+2 -29
View File
@@ -2,7 +2,6 @@ package mux
import (
"context"
"encoding/binary"
goerrors "errors"
"io"
"sync"
@@ -10,7 +9,6 @@ import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
@@ -172,8 +170,7 @@ func (f *DialingWorkerFactory) Create() (*ClientWorker, error) {
type ClientStrategy struct {
MaxConcurrency uint32
MaxReuseTimes uint32
BrutalBPS uint64
MaxConnection uint32
}
type ClientWorker struct {
@@ -201,9 +198,6 @@ func NewClientWorker(stream transport.Link, s ClientStrategy) (*ClientWorker, er
go c.fetchOutput()
go c.monitor()
if s.BrutalBPS > 0 {
go c.sendSetBrutal(s.BrutalBPS)
}
return c, nil
}
@@ -294,7 +288,7 @@ func fetchInput(ctx context.Context, s *Session, output buf.Writer) {
func (m *ClientWorker) IsClosing() bool {
sm := m.sessionManager
if m.strategy.MaxReuseTimes > 0 && sm.Count() >= int(m.strategy.MaxReuseTimes) {
if m.strategy.MaxConnection > 0 && sm.Count() >= int(m.strategy.MaxConnection) {
return true
}
return false
@@ -423,24 +417,3 @@ func (m *ClientWorker) fetchOutput() {
}
}
}
func (m *ClientWorker) sendSetBrutal(sendBPS uint64) {
meta := FrameMetadata{
SessionID: 91,
SessionStatus: SessionStatusSetBrutal,
}
meta.Option.Set(OptionData)
frame := buf.New()
common.Must(meta.WriteTo(frame))
lengthByte := frame.Extend(2)
speedByte := frame.Extend(int32(200 + dice.Roll(200)))
binary.BigEndian.PutUint64(speedByte, sendBPS)
binary.BigEndian.PutUint16(lengthByte, uint16(len(speedByte)))
errors.LogError(context.Background(), "Start sending SetBrutal frame with speed: ", sendBPS)
err := m.link.Writer.WriteMultiBuffer(buf.MultiBuffer{frame})
if err != nil {
frame.Release()
errors.LogError(context.Background(), "failed to send SetBrutal frame: ", err)
}
errors.LogInfo(context.Background(), "SetBrutal frame sent successfully")
}
+2 -2
View File
@@ -58,7 +58,7 @@ func TestClientWorkerClose(t *testing.T) {
Writer: w1,
}, mux.ClientStrategy{
MaxConcurrency: 4,
MaxReuseTimes: 4,
MaxConnection: 4,
})
common.Must(err)
@@ -68,7 +68,7 @@ func TestClientWorkerClose(t *testing.T) {
Writer: w2,
}, mux.ClientStrategy{
MaxConcurrency: 4,
MaxReuseTimes: 4,
MaxConnection: 4,
})
common.Must(err)
-2
View File
@@ -21,8 +21,6 @@ const (
SessionStatusKeep SessionStatus = 0x02
SessionStatusEnd SessionStatus = 0x03
SessionStatusKeepAlive SessionStatus = 0x04
SessionStatusSetBrutal SessionStatus = 0x91
)
const (
-38
View File
@@ -2,7 +2,6 @@ package mux
import (
"context"
"encoding/binary"
"io"
"time"
@@ -17,7 +16,6 @@ import (
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet/brutal"
"github.com/xtls/xray-core/transport/pipe"
)
@@ -335,40 +333,6 @@ func (w *ServerWorker) handleStatusEnd(meta *FrameMetadata, reader *buf.Buffered
return nil
}
// note: do not return error, just log it
func (w *ServerWorker) HandleSetBrutal(ctx context.Context, meta *FrameMetadata, reader *buf.BufferedReader) error {
if meta.Option.Has(OptionData) == false {
errors.LogError(ctx, "SetBrutal frame missing data")
return nil
}
chunkReader := NewStreamReader(reader)
data, err := chunkReader.ReadMultiBuffer()
if err != nil {
errors.LogError(ctx, "unexpected error when reading brutal data: ", err)
}
speed := binary.BigEndian.Uint64(data.Bytes())
inbound := session.InboundFromContext(ctx)
if inbound == nil || inbound.Conn == nil {
errors.LogError(ctx, "no inbound connection found for brutal set")
return nil
}
conn := inbound.Conn
tcpConn, ok := conn.(*net.TCPConn)
if !ok {
errors.LogError(ctx, "brutal can only be set on TCP connections")
return nil
}
err = brutal.SetBrutal(tcpConn, speed)
if err != nil {
errors.LogError(ctx, "failed to set brutal: ", err)
return nil
} else {
errors.LogInfo(ctx, "successfully set brutal speed: ", speed)
}
return nil
}
func (w *ServerWorker) handleFrame(ctx context.Context, reader *buf.BufferedReader) error {
var meta FrameMetadata
err := meta.Unmarshal(reader, session.IsReverseMuxFromContext(ctx))
@@ -385,8 +349,6 @@ func (w *ServerWorker) handleFrame(ctx context.Context, reader *buf.BufferedRead
err = w.handleStatusNew(session.ContextWithIsReverseMux(ctx, false), &meta, reader)
case SessionStatusKeep:
err = w.handleStatusKeep(&meta, reader)
case SessionStatusSetBrutal:
err = w.HandleSetBrutal(ctx, &meta, reader)
default:
status := meta.SessionStatus
return errors.New("unknown status: ", status).AtError()
+2 -3
View File
@@ -56,12 +56,11 @@ func (m *SessionManager) Allocate(Strategy *ClientStrategy) *Session {
defer m.Unlock()
MaxConcurrency := int(Strategy.MaxConcurrency)
MaxReuseTimes := uint16(Strategy.MaxReuseTimes)
MaxConnection := uint16(Strategy.MaxConnection)
if m.closed || (MaxConcurrency > 0 && len(m.sessions) >= MaxConcurrency) || (MaxReuseTimes > 0 && m.count >= MaxReuseTimes) {
if m.closed || (MaxConcurrency > 0 && len(m.sessions) >= MaxConcurrency) || (MaxConnection > 0 && m.count >= MaxConnection) {
return nil
}
errors.LogInfo(context.Background(), "Allocated mux.cool session id ", m.count, "/", MaxReuseTimes)
m.count++
s := &Session{
-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 -30
View File
@@ -12,7 +12,6 @@ package core
import (
"fmt"
"runtime"
"runtime/debug"
"github.com/xtls/xray-core/common/serial"
)
@@ -20,7 +19,7 @@ import (
var (
Version_x byte = 26
Version_y byte = 2
Version_z byte = 6
Version_z byte = 4
)
var (
@@ -29,34 +28,6 @@ var (
intro = "A unified platform for anti-censorship."
)
func init() {
// Manually injected
if build != "Custom" {
return
}
info, ok := debug.ReadBuildInfo()
if !ok {
return
}
var isDirty bool
var foundBuild bool
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.revision":
if len(setting.Value) < 7 {
return
}
build = setting.Value[:7]
foundBuild = true
case "vcs.modified":
isDirty = setting.Value == "true"
}
}
if isDirty && foundBuild {
build += "-dirty"
}
}
// Version returns Xray's version as a string, in the form of "x.y.z" where x, y and z are numbers.
// ".z" part may be omitted in regular releases.
func Version() string {
+9 -9
View File
@@ -1,6 +1,6 @@
module github.com/xtls/xray-core
go 1.26
go 1.25.6
require (
github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178
@@ -9,10 +9,9 @@ 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.3.0
github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5
github.com/pires/go-proxyproto v0.11.0
github.com/pires/go-proxyproto v0.9.2
github.com/refraction-networking/utls v1.8.2
github.com/sagernet/sing v0.5.1
github.com/sagernet/sing-shadowsocks v0.2.7
@@ -20,11 +19,11 @@ require (
github.com/vishvananda/netlink v1.3.1
github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.48.0
golang.org/x/crypto v0.47.0
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.50.0
golang.org/x/net v0.49.0
golang.org/x/sync v0.19.0
golang.org/x/sys v0.41.0
golang.org/x/sys v0.40.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
google.golang.org/grpc v1.78.0
@@ -40,14 +39,15 @@ 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
github.com/vishvananda/netns v0.0.5 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.41.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+16 -16
View File
@@ -31,8 +31,8 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -43,8 +43,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pires/go-proxyproto v0.9.2 h1:H1UdHn695zUVVmB0lQ354lOWHOy6TZSpzBl3tgN0s1U=
github.com/pires/go-proxyproto v0.9.2/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
@@ -84,18 +84,18 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -107,21 +107,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-32
View File
@@ -3,9 +3,7 @@ package conf
import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/proxy/hysteria"
"github.com/xtls/xray-core/proxy/hysteria/account"
"google.golang.org/protobuf/proto"
)
@@ -29,33 +27,3 @@ func (c *HysteriaClientConfig) Build() (proto.Message, error) {
return config, nil
}
type HysteriaUserConfig struct {
Auth string `json:"auth"`
Level uint32 `json:"level"`
Email string `json:"email"`
}
type HysteriaServerConfig struct {
Version int32 `json:"version"`
Users []*HysteriaUserConfig `json:"clients"`
}
func (c *HysteriaServerConfig) Build() (proto.Message, error) {
config := new(hysteria.ServerConfig)
if c.Users != nil {
for _, user := range c.Users {
account := &account.Account{
Auth: user.Auth,
}
config.Users = append(config.Users, &protocol.User{
Email: user.Email,
Level: user.Level,
Account: serial.ToTypedMessage(account),
})
}
}
return config, nil
}
+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",
+51 -42
View File
@@ -37,6 +37,7 @@ import (
"github.com/xtls/xray-core/transport/internet/tcp"
"github.com/xtls/xray-core/transport/internet/tls"
"github.com/xtls/xray-core/transport/internet/websocket"
"github.com/xtls/xray-core/transport/internet/xdrive"
"google.golang.org/protobuf/proto"
)
@@ -452,6 +453,41 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
return config, nil
}
// XDriveConfig represents the configuration for the XDRIVE transport.
// For the "local" service, secrets[0] should be "client" for client-side
// or "server" for server-side.
// For "Google Drive" service, secrets should contain [role, ClientID, ClientSecret, RefreshToken]
// where role is "client" or "server".
type XDriveConfig struct {
RemoteFolder string `json:"remoteFolder"`
Service string `json:"service"`
Secrets []string `json:"secrets"`
}
// Build implements Buildable.
func (c *XDriveConfig) Build() (proto.Message, error) {
switch c.Service {
case "local":
// For local service, secrets[0] must be "client" or "server"
if len(c.Secrets) < 1 {
return nil, errors.New("local service needs secrets[0] set to 'client' or 'server'")
}
case "Google Drive":
// For Google Drive, secrets should be [role, ClientID, ClientSecret, RefreshToken]
if len(c.Secrets) != 4 {
return nil, errors.New("Google Drive needs 4 secrets: role ('client' or 'server'), ClientID, ClientSecret, RefreshToken")
}
default:
return nil, errors.New("unsupported service")
}
config := &xdrive.Config{
RemoteFolder: c.RemoteFolder,
Service: c.Service,
Secrets: c.Secrets,
}
return config, nil
}
const (
Byte = 1
Kilobyte = 1024 * Byte
@@ -508,20 +544,6 @@ type UdpHop struct {
Interval *Int32Range `json:"interval"`
}
type Masquerade struct {
Type string `json:"type"`
Dir string `json:"dir"`
Url string `json:"url"`
RewriteHost bool `json:"rewriteHost"`
Insecure bool `json:"insecure"`
Content string `json:"content"`
Headers map[string]string `json:"headers"`
StatusCode int32 `json:"statusCode"`
}
type HysteriaConfig struct {
Version int32 `json:"version"`
Auth string `json:"auth"`
@@ -537,10 +559,6 @@ type HysteriaConfig struct {
MaxIdleTimeout int64 `json:"maxIdleTimeout"`
KeepAlivePeriod int64 `json:"keepAlivePeriod"`
DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"`
MaxIncomingStreams int64 `json:"maxIncomingStreams"`
UdpIdleTimeout int64 `json:"udpIdleTimeout"`
Masquerade Masquerade `json:"masquerade"`
}
func (c *HysteriaConfig) Build() (proto.Message, error) {
@@ -574,10 +592,10 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
}
if up > 0 && up < 65536 {
return nil, errors.New("Up must be at least 65536 bytes per second")
return nil, errors.New("Up must be at least 65536 Bps")
}
if down > 0 && down < 65536 {
return nil, errors.New("Down must be at least 65536 bytes per second")
return nil, errors.New("Down must be at least 65536 Bps")
}
if (inertvalMin != 0 && inertvalMin < 5) || (inertvalMax != 0 && inertvalMax < 5) {
return nil, errors.New("Interval must be at least 5")
@@ -601,12 +619,6 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
if c.KeepAlivePeriod != 0 && (c.KeepAlivePeriod < 2 || c.KeepAlivePeriod > 60) {
return nil, errors.New("KeepAlivePeriod must be between 2 and 60")
}
if c.MaxIncomingStreams != 0 && c.MaxIncomingStreams < 8 {
return nil, errors.New("MaxIncomingStreams must be at least 8")
}
if c.UdpIdleTimeout != 0 && (c.UdpIdleTimeout < 2 || c.UdpIdleTimeout > 600) {
return nil, errors.New("UdpIdleTimeout must be between 2 and 600")
}
config := &hysteria.Config{}
config.Version = c.Version
@@ -624,16 +636,6 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
config.MaxIdleTimeout = c.MaxIdleTimeout
config.KeepAlivePeriod = c.KeepAlivePeriod
config.DisablePathMtuDiscovery = c.DisablePathMTUDiscovery
config.MaxIncomingStreams = c.MaxIncomingStreams
config.UdpIdleTimeout = c.UdpIdleTimeout
config.MasqType = c.Masquerade.Type
config.MasqFile = c.Masquerade.Dir
config.MasqUrl = c.Masquerade.Url
config.MasqUrlRewriteHost = c.Masquerade.RewriteHost
config.MasqUrlInsecure = c.Masquerade.Insecure
config.MasqString = c.Masquerade.Content
config.MasqStringHeaders = c.Masquerade.Headers
config.MasqStringStatusCode = c.Masquerade.StatusCode
if config.InitStreamReceiveWindow == 0 {
config.InitStreamReceiveWindow = 8388608
@@ -653,12 +655,6 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
// if config.KeepAlivePeriod == 0 {
// config.KeepAlivePeriod = 10
// }
if config.MaxIncomingStreams == 0 {
config.MaxIncomingStreams = 1024
}
if config.UdpIdleTimeout == 0 {
config.UdpIdleTimeout = 60
}
return config, nil
}
@@ -1094,6 +1090,8 @@ func (p TransportProtocol) Build() (string, error) {
return "", errors.PrintRemovedFeatureError("QUIC transport (without web service, etc.)", "XHTTP stream-one H3")
case "hysteria":
return "hysteria", nil
case "xdrive":
return "xdrive", nil
default:
return "", errors.New("Config: unknown transport protocol: ", p)
}
@@ -1443,6 +1441,7 @@ type StreamConfig struct {
WSSettings *WebSocketConfig `json:"wsSettings"`
HTTPUPGRADESettings *HttpUpgradeConfig `json:"httpupgradeSettings"`
HysteriaSettings *HysteriaConfig `json:"hysteriaSettings"`
XDriveSettings *XDriveConfig `json:"xdriveSettings"`
SocketSettings *SocketConfig `json:"sockopt"`
}
@@ -1573,6 +1572,16 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
Settings: serial.ToTypedMessage(hs),
})
}
if c.XDriveSettings != nil {
xs, err := c.XDriveSettings.Build()
if err != nil {
return nil, errors.New("Failed to build XDrive config.").Base(err)
}
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
ProtocolName: "xdrive",
Settings: serial.ToTypedMessage(xs),
})
}
if c.SocketSettings != nil {
ss, err := c.SocketSettings.Build()
if err != nil {
-3
View File
@@ -33,7 +33,6 @@ var (
"vmess": func() interface{} { return new(VMessInboundConfig) },
"trojan": func() interface{} { return new(TrojanServerConfig) },
"wireguard": func() interface{} { return &WireGuardConfig{IsClient: false} },
"hysteria": func() interface{} { return new(HysteriaServerConfig) },
"tun": func() interface{} { return new(TunConfig) },
}, "protocol", "settings")
@@ -102,7 +101,6 @@ func (c *SniffingConfig) Build() (*proxyman.SniffingConfig, error) {
type MuxConfig struct {
Enabled bool `json:"enabled"`
Concurrency int16 `json:"concurrency"`
BrutalBPS uint64 `json:"brutalBPS"`
XudpConcurrency int16 `json:"xudpConcurrency"`
XudpProxyUDP443 string `json:"xudpProxyUDP443"`
}
@@ -119,7 +117,6 @@ func (m *MuxConfig) Build() (*proxyman.MultiplexingConfig, error) {
return &proxyman.MultiplexingConfig{
Enabled: m.Enabled,
Concurrency: int32(m.Concurrency),
BrutalBPS: m.BrutalBPS,
XudpConcurrency: int32(m.XudpConcurrency),
XudpProxyUDP443: m.XudpProxyUDP443,
}, nil
+4 -90
View File
@@ -1,14 +1,11 @@
package tls
import (
"crypto/ecdh"
"crypto/hpke"
"crypto/rand"
"encoding/base64"
"encoding/pem"
"io"
"os"
"github.com/xtls/reality/hpke"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/main/commands/base"
"github.com/xtls/xray-core/transport/internet/tls"
@@ -43,15 +40,15 @@ func executeECH(cmd *base.Command, args []string) {
// if *input_pqSignatureSchemesEnabled {
// kem = 0x30 // hpke.KEM_X25519_KYBER768_DRAFT00
// } else {
kem = hpke.DHKEM(ecdh.X25519()).ID()
kem = hpke.DHKEM_X25519_HKDF_SHA256
// }
echConfig, priv, err := generateECHKeySet(0, *input_serverName, kem)
echConfig, priv, err := tls.GenerateECHKeySet(0, *input_serverName, kem)
common.Must(err)
var configBuffer, keyBuffer []byte
if *input_echServerKeys == "" {
configBytes, _ := marshalBinary(echConfig)
configBytes, _ := tls.MarshalBinary(echConfig)
var b cryptobyte.Builder
b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
child.AddBytes(configBytes)
@@ -94,86 +91,3 @@ func executeECH(cmd *base.Command, args []string) {
os.Stdout.WriteString("ECH server keys: \n" + base64.StdEncoding.EncodeToString(keyBuffer) + "\n")
}
}
type EchConfig struct {
Version uint16
ConfigID uint8
KemID uint16
PublicKey []byte
SymmetricCipherSuite []EchCipher
MaxNameLength uint8
PublicName []byte
Extensions []Extension
}
type EchCipher struct {
KDFID uint16
AEADID uint16
}
type Extension struct {
Type uint16
Data []byte
}
// reference github.com/OmarTariq612/goech
func marshalBinary(ech EchConfig) ([]byte, error) {
var b cryptobyte.Builder
b.AddUint16(ech.Version)
b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
child.AddUint8(ech.ConfigID)
child.AddUint16(ech.KemID)
child.AddUint16(uint16(len(ech.PublicKey)))
child.AddBytes(ech.PublicKey)
child.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
for _, cipherSuite := range ech.SymmetricCipherSuite {
child.AddUint16(cipherSuite.KDFID)
child.AddUint16(cipherSuite.AEADID)
}
})
child.AddUint8(ech.MaxNameLength)
child.AddUint8(uint8(len(ech.PublicName)))
child.AddBytes(ech.PublicName)
child.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
for _, extention := range ech.Extensions {
child.AddUint16(extention.Type)
child.AddBytes(extention.Data)
}
})
})
return b.Bytes()
}
const ExtensionEncryptedClientHello = 0xfe0d
func generateECHKeySet(configID uint8, domain string, kem uint16) (EchConfig, []byte, error) {
config := EchConfig{
Version: ExtensionEncryptedClientHello,
ConfigID: configID,
PublicName: []byte(domain),
KemID: kem,
SymmetricCipherSuite: []EchCipher{
{KDFID: hpke.HKDFSHA256().ID(), AEADID: hpke.AES128GCM().ID()},
{KDFID: hpke.HKDFSHA256().ID(), AEADID: hpke.AES256GCM().ID()},
{KDFID: hpke.HKDFSHA256().ID(), AEADID: hpke.ChaCha20Poly1305().ID()},
{KDFID: hpke.HKDFSHA384().ID(), AEADID: hpke.AES128GCM().ID()},
{KDFID: hpke.HKDFSHA384().ID(), AEADID: hpke.AES256GCM().ID()},
{KDFID: hpke.HKDFSHA384().ID(), AEADID: hpke.ChaCha20Poly1305().ID()},
{KDFID: hpke.HKDFSHA512().ID(), AEADID: hpke.AES128GCM().ID()},
{KDFID: hpke.HKDFSHA512().ID(), AEADID: hpke.AES256GCM().ID()},
{KDFID: hpke.HKDFSHA512().ID(), AEADID: hpke.ChaCha20Poly1305().ID()},
},
MaxNameLength: 0,
Extensions: nil,
}
// if kem == hpke.DHKEM_X25519_HKDF_SHA256 {
curve := ecdh.X25519()
priv := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, priv)
if err != nil {
return config, nil, err
}
privKey, _ := curve.NewPrivateKey(priv)
config.PublicKey = privKey.PublicKey().Bytes()
return config, priv, 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)
}
+8 -8
View File
@@ -135,15 +135,15 @@ func printCertificates(tabWriter *tabwriter.Writer, certs []*x509.Certificate) {
CAs = append(CAs, cert)
}
}
fmt.Fprintf(tabWriter, "Certificate chain's total length:\t%d (certs count: %s)\n", length, strconv.Itoa(len(certs)))
fmt.Fprintf(tabWriter, "Certificate chain's total length: \t %d (certs count: %s)\n", length, 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)))
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 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.Fprintf(tabWriter, "Cert's allowed domains: \t %v\n", leaf.DNSNames)
}
}
@@ -156,11 +156,11 @@ func printTLSConnDetail(tabWriter *tabwriter.Writer, tlsConn *utls.UConn) {
case gotls.VersionTLS12:
tlsVersion = "TLS 1.2"
}
fmt.Fprintf(tabWriter, "TLS Version:\t%s\n", tlsVersion)
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.Fprintf(tabWriter, "TLS Post-Quantum key exchange: \t %t (%s)\n", PostQuantum, curveID.String())
} else {
fmt.Fprintf(tabWriter, "TLS Post-Quantum key exchange: false (RSA Exchange)\n")
}
+1 -1
View File
@@ -13,7 +13,7 @@ var CmdTLS = &base.Command{
Commands: []*base.Command{
cmdCert,
cmdPing,
cmdHash,
cmdLeafCertHash,
cmdECH,
},
}
+1
View File
@@ -59,6 +59,7 @@ import (
_ "github.com/xtls/xray-core/transport/internet/tls"
_ "github.com/xtls/xray-core/transport/internet/udp"
_ "github.com/xtls/xray-core/transport/internet/websocket"
_ "github.com/xtls/xray-core/transport/internet/xdrive"
// Transport headers
_ "github.com/xtls/xray-core/transport/internet/headers/http"
-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")
-129
View File
@@ -1,129 +0,0 @@
package account
import (
"sync"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol"
"google.golang.org/protobuf/proto"
)
func (a *Account) AsAccount() (protocol.Account, error) {
return &MemoryAccount{
Auth: a.Auth,
}, nil
}
type MemoryAccount struct {
Auth string
}
func (a *MemoryAccount) Equals(another protocol.Account) bool {
if account, ok := another.(*MemoryAccount); ok {
return a.Auth == account.Auth
}
return false
}
func (a *MemoryAccount) ToProto() proto.Message {
return &Account{
Auth: a.Auth,
}
}
type Validator struct {
emails map[string]struct{}
users map[string]*protocol.MemoryUser
mutex sync.Mutex
}
func NewValidator() *Validator {
return &Validator{
emails: make(map[string]struct{}),
users: make(map[string]*protocol.MemoryUser),
}
}
func (v *Validator) Add(u *protocol.MemoryUser) error {
v.mutex.Lock()
defer v.mutex.Unlock()
if u.Email != "" {
if _, ok := v.emails[u.Email]; ok {
return errors.New("User ", u.Email, " already exists.")
}
v.emails[u.Email] = struct{}{}
}
v.users[u.Account.(*MemoryAccount).Auth] = u
return nil
}
func (v *Validator) Del(email string) error {
if email == "" {
return errors.New("Email must not be empty.")
}
v.mutex.Lock()
defer v.mutex.Unlock()
if _, ok := v.emails[email]; !ok {
return errors.New("User ", email, " not found.")
}
delete(v.emails, email)
for key, user := range v.users {
if user.Email == email {
delete(v.users, key)
break
}
}
return nil
}
func (v *Validator) Get(auth string) *protocol.MemoryUser {
v.mutex.Lock()
defer v.mutex.Unlock()
return v.users[auth]
}
func (v *Validator) GetByEmail(email string) *protocol.MemoryUser {
if email == "" {
return nil
}
v.mutex.Lock()
defer v.mutex.Unlock()
if _, ok := v.emails[email]; ok {
for _, user := range v.users {
if user.Email == email {
return user
}
}
}
return nil
}
func (v *Validator) GetAll() []*protocol.MemoryUser {
v.mutex.Lock()
defer v.mutex.Unlock()
var users = make([]*protocol.MemoryUser, 0, len(v.users))
for _, user := range v.users {
users = append(users, user)
}
return users
}
func (v *Validator) GetCount() int64 {
v.mutex.Lock()
defer v.mutex.Unlock()
return int64(len(v.users))
}
-123
View File
@@ -1,123 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.5
// source: proxy/hysteria/account/config.proto
package account
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
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 Account struct {
state protoimpl.MessageState `protogen:"open.v1"`
Auth string `protobuf:"bytes,1,opt,name=auth,proto3" json:"auth,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Account) Reset() {
*x = Account{}
mi := &file_proxy_hysteria_account_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Account) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Account) ProtoMessage() {}
func (x *Account) ProtoReflect() protoreflect.Message {
mi := &file_proxy_hysteria_account_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 Account.ProtoReflect.Descriptor instead.
func (*Account) Descriptor() ([]byte, []int) {
return file_proxy_hysteria_account_config_proto_rawDescGZIP(), []int{0}
}
func (x *Account) GetAuth() string {
if x != nil {
return x.Auth
}
return ""
}
var File_proxy_hysteria_account_config_proto protoreflect.FileDescriptor
const file_proxy_hysteria_account_config_proto_rawDesc = "" +
"\n" +
"#proxy/hysteria/account/config.proto\x12\x1bxray.proxy.hysteria.account\"\x1d\n" +
"\aAccount\x12\x12\n" +
"\x04auth\x18\x01 \x01(\tR\x04authBs\n" +
"\x1fcom.xray.proxy.hysteria.accountP\x01Z0github.com/xtls/xray-core/proxy/hysteria/account\xaa\x02\x1bXray.Proxy.Hysteria.Accountb\x06proto3"
var (
file_proxy_hysteria_account_config_proto_rawDescOnce sync.Once
file_proxy_hysteria_account_config_proto_rawDescData []byte
)
func file_proxy_hysteria_account_config_proto_rawDescGZIP() []byte {
file_proxy_hysteria_account_config_proto_rawDescOnce.Do(func() {
file_proxy_hysteria_account_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proxy_hysteria_account_config_proto_rawDesc), len(file_proxy_hysteria_account_config_proto_rawDesc)))
})
return file_proxy_hysteria_account_config_proto_rawDescData
}
var file_proxy_hysteria_account_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_proxy_hysteria_account_config_proto_goTypes = []any{
(*Account)(nil), // 0: xray.proxy.hysteria.account.Account
}
var file_proxy_hysteria_account_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_proxy_hysteria_account_config_proto_init() }
func file_proxy_hysteria_account_config_proto_init() {
if File_proxy_hysteria_account_config_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_hysteria_account_config_proto_rawDesc), len(file_proxy_hysteria_account_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_proxy_hysteria_account_config_proto_goTypes,
DependencyIndexes: file_proxy_hysteria_account_config_proto_depIdxs,
MessageInfos: file_proxy_hysteria_account_config_proto_msgTypes,
}.Build()
File_proxy_hysteria_account_config_proto = out.File
file_proxy_hysteria_account_config_proto_goTypes = nil
file_proxy_hysteria_account_config_proto_depIdxs = nil
}
-11
View File
@@ -1,11 +0,0 @@
syntax = "proto3";
package xray.proxy.hysteria.account;
option csharp_namespace = "Xray.Proxy.Hysteria.Account";
option go_package = "github.com/xtls/xray-core/proxy/hysteria/account";
option java_package = "com.xray.proxy.hysteria.account";
option java_multiple_files = true;
message Account {
string auth = 1;
}
+25 -44
View File
@@ -135,7 +135,6 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
if err := buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)); err != nil {
return errors.New("failed to transport all UDP request").Base(err)
}
return nil
}
@@ -144,14 +143,12 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
reader := &UDPReader{
Reader: conn,
buf: make([]byte, MaxUDPSize),
df: &Defragger{},
}
if err := buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)); err != nil {
return errors.New("failed to transport all UDP response").Base(err)
}
return nil
}
@@ -181,6 +178,7 @@ type UDPWriter struct {
func (w *UDPWriter) sendMsg(msg *UDPMessage) error {
msgN := msg.Serialize(w.buf)
if msgN < 0 {
// Message larger than buffer, silent drop
return nil
}
_, err := w.Writer.Write(w.buf[:msgN])
@@ -194,12 +192,10 @@ func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
if b == nil {
break
}
addr := w.addr
if b.UDP != nil {
addr = b.UDP.NetAddr()
}
msg := &UDPMessage{
SessionID: 0,
PacketID: 0,
@@ -208,58 +204,47 @@ func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
Addr: addr,
Data: b.Bytes(),
}
err := w.sendMsg(msg)
var errTooLarge *quic.DatagramTooLargeError
if go_errors.As(err, &errTooLarge) {
msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1
fMsgs := FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize))
for _, fMsg := range fMsgs {
err := w.sendMsg(&fMsg)
if err != nil {
b.Release()
buf.ReleaseMulti(mb)
return err
if err := w.sendMsg(msg); err != nil {
var errTooLarge *quic.DatagramTooLargeError
if go_errors.As(err, &errTooLarge) {
msg.PacketID = uint16(rand.Intn(0xFFFF)) + 1
fMsgs := FragUDPMessage(msg, int(errTooLarge.MaxDatagramPayloadSize))
for _, fMsg := range fMsgs {
err := w.sendMsg(&fMsg)
if err != nil {
b.Release()
buf.ReleaseMulti(mb)
return err
}
}
} else {
b.Release()
buf.ReleaseMulti(mb)
return err
}
} else if err != nil {
b.Release()
buf.ReleaseMulti(mb)
return err
}
b.Release()
}
return nil
}
type UDPReader struct {
Reader io.Reader
buf []byte
df *Defragger
firstMsg *UDPMessage
firstDest *net.Destination
Reader io.Reader
df *Defragger
}
func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
if r.firstMsg != nil {
buffer := buf.New()
buffer.Write(r.firstMsg.Data)
buffer.UDP = r.firstDest
r.firstMsg = nil
return buf.MultiBuffer{buffer}, nil
}
for {
n, err := r.Reader.Read(r.buf)
b := buf.New()
_, err := b.ReadFrom(r.Reader)
if err != nil {
b.Release()
return nil, err
}
msg, err := ParseUDPMessage(r.buf[:n])
msg, err := ParseUDPMessage(b.Bytes())
if err != nil {
b.Release()
continue
}
@@ -268,11 +253,7 @@ func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
continue
}
dest, err := net.ParseDestination("udp:" + dfMsg.Addr)
if err != nil {
errors.LogDebug(context.Background(), dfMsg.Addr, " ParseDestination err ", err)
continue
}
dest, _ := net.ParseDestination("udp:" + dfMsg.Addr)
buffer := buf.New()
buffer.Write(dfMsg.Data)
+2 -2
View File
@@ -5,6 +5,6 @@ import (
)
var (
tcpRequestPadding = padding.Padding{Min: 64, Max: 512}
tcpResponsePadding = padding.Padding{Min: 128, Max: 1024}
tcpRequestPadding = padding.Padding{Min: 64, Max: 512}
// tcpResponsePadding = padding.Padding{Min: 128, Max: 1024}
)
+11 -60
View File
@@ -74,60 +74,14 @@ func (x *ClientConfig) GetServer() *protocol.ServerEndpoint {
return nil
}
type ServerConfig struct {
state protoimpl.MessageState `protogen:"open.v1"`
Users []*protocol.User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ServerConfig) Reset() {
*x = ServerConfig{}
mi := &file_proxy_hysteria_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ServerConfig) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ServerConfig) ProtoMessage() {}
func (x *ServerConfig) ProtoReflect() protoreflect.Message {
mi := &file_proxy_hysteria_config_proto_msgTypes[1]
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 ServerConfig.ProtoReflect.Descriptor instead.
func (*ServerConfig) Descriptor() ([]byte, []int) {
return file_proxy_hysteria_config_proto_rawDescGZIP(), []int{1}
}
func (x *ServerConfig) GetUsers() []*protocol.User {
if x != nil {
return x.Users
}
return nil
}
var File_proxy_hysteria_config_proto protoreflect.FileDescriptor
const file_proxy_hysteria_config_proto_rawDesc = "" +
"\n" +
"\x1bproxy/hysteria/config.proto\x12\x13xray.proxy.hysteria\x1a!common/protocol/server_spec.proto\x1a\x1acommon/protocol/user.proto\"f\n" +
"\x1bproxy/hysteria/config.proto\x12\x13xray.proxy.hysteria\x1a!common/protocol/server_spec.proto\"f\n" +
"\fClientConfig\x12\x18\n" +
"\aversion\x18\x01 \x01(\x05R\aversion\x12<\n" +
"\x06server\x18\x02 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"@\n" +
"\fServerConfig\x120\n" +
"\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05usersB[\n" +
"\x06server\x18\x02 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06serverB[\n" +
"\x17com.xray.proxy.hysteriaP\x01Z(github.com/xtls/xray-core/proxy/hysteria\xaa\x02\x13Xray.Proxy.Hysteriab\x06proto3"
var (
@@ -142,21 +96,18 @@ func file_proxy_hysteria_config_proto_rawDescGZIP() []byte {
return file_proxy_hysteria_config_proto_rawDescData
}
var file_proxy_hysteria_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proxy_hysteria_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_proxy_hysteria_config_proto_goTypes = []any{
(*ClientConfig)(nil), // 0: xray.proxy.hysteria.ClientConfig
(*ServerConfig)(nil), // 1: xray.proxy.hysteria.ServerConfig
(*protocol.ServerEndpoint)(nil), // 2: xray.common.protocol.ServerEndpoint
(*protocol.User)(nil), // 3: xray.common.protocol.User
(*protocol.ServerEndpoint)(nil), // 1: xray.common.protocol.ServerEndpoint
}
var file_proxy_hysteria_config_proto_depIdxs = []int32{
2, // 0: xray.proxy.hysteria.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint
3, // 1: xray.proxy.hysteria.ServerConfig.users:type_name -> xray.common.protocol.User
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
1, // 0: xray.proxy.hysteria.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_proxy_hysteria_config_proto_init() }
@@ -170,7 +121,7 @@ func file_proxy_hysteria_config_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_hysteria_config_proto_rawDesc), len(file_proxy_hysteria_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
-5
View File
@@ -7,13 +7,8 @@ option java_package = "com.xray.proxy.hysteria";
option java_multiple_files = true;
import "common/protocol/server_spec.proto";
import "common/protocol/user.proto";
message ClientConfig {
int32 version = 1;
xray.common.protocol.ServerEndpoint server = 2;
}
message ServerConfig {
repeated xray.common.protocol.User users = 1;
}
-12
View File
@@ -2,15 +2,12 @@ package ctx
import (
"context"
"github.com/xtls/xray-core/proxy/hysteria/account"
)
type key int
const (
requireDatagram key = iota
validator
)
func ContextWithRequireDatagram(ctx context.Context, udp bool) context.Context {
@@ -24,12 +21,3 @@ func RequireDatagramFromContext(ctx context.Context) bool {
_, ok := ctx.Value(requireDatagram).(struct{})
return ok
}
func ContextWithValidator(ctx context.Context, v *account.Validator) context.Context {
return context.WithValue(ctx, validator, v)
}
func ValidatorFromContext(ctx context.Context) *account.Validator {
v, _ := ctx.Value(validator).(*account.Validator)
return v
}
+7 -52
View File
@@ -11,6 +11,8 @@ import (
)
const (
FrameTypeTCPRequest = 0x401
// Max length values are for preventing DoS attacks
MaxAddressLength = 2048
@@ -26,49 +28,22 @@ const (
)
// TCPRequest format:
// 0x401 (QUIC varint)
// Address length (QUIC varint)
// Address (bytes)
// Padding length (QUIC varint)
// Padding (bytes)
func ReadTCPRequest(r io.Reader) (string, error) {
bReader := quicvarint.NewReader(r)
addrLen, err := quicvarint.Read(bReader)
if err != nil {
return "", err
}
if addrLen == 0 || addrLen > MaxAddressLength {
return "", errors.New("invalid address length")
}
addrBuf := make([]byte, addrLen)
_, err = io.ReadFull(r, addrBuf)
if err != nil {
return "", err
}
paddingLen, err := quicvarint.Read(bReader)
if err != nil {
return "", err
}
if paddingLen > MaxPaddingLength {
return "", errors.New("invalid padding length")
}
if paddingLen > 0 {
_, err = io.CopyN(io.Discard, r, int64(paddingLen))
if err != nil {
return "", err
}
}
return string(addrBuf), nil
}
func WriteTCPRequest(w io.Writer, addr string) error {
padding := tcpRequestPadding.String()
paddingLen := len(padding)
addrLen := len(addr)
sz := int(quicvarint.Len(uint64(addrLen))) + addrLen +
sz := int(quicvarint.Len(FrameTypeTCPRequest)) +
int(quicvarint.Len(uint64(addrLen))) + addrLen +
int(quicvarint.Len(uint64(paddingLen))) + paddingLen
buf := make([]byte, sz)
i := varintPut(buf, uint64(addrLen))
i := varintPut(buf, FrameTypeTCPRequest)
i += varintPut(buf[i:], uint64(addrLen))
i += copy(buf[i:], addr)
i += varintPut(buf[i:], uint64(paddingLen))
copy(buf[i:], padding)
@@ -121,26 +96,6 @@ func ReadTCPResponse(r io.Reader) (bool, string, error) {
return status[0] == 0, string(msgBuf), nil
}
func WriteTCPResponse(w io.Writer, ok bool, msg string) error {
padding := tcpResponsePadding.String()
paddingLen := len(padding)
msgLen := len(msg)
sz := 1 + int(quicvarint.Len(uint64(msgLen))) + msgLen +
int(quicvarint.Len(uint64(paddingLen))) + paddingLen
buf := make([]byte, sz)
if ok {
buf[0] = 0
} else {
buf[0] = 1
}
i := varintPut(buf[1:], uint64(msgLen))
i += copy(buf[1+i:], msg)
i += varintPut(buf[1+i:], uint64(paddingLen))
copy(buf[1+i:], padding)
_, err := w.Write(buf)
return err
}
// UDPMessage format:
// Session ID (uint32 BE)
// Packet ID (uint16 BE)
-198
View File
@@ -1,198 +0,0 @@
package hysteria
import (
"context"
"io"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/policy"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/proxy/hysteria/account"
"github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet/hysteria"
"github.com/xtls/xray-core/transport/internet/stat"
)
type Server struct {
config *ServerConfig
validator *account.Validator
policyManager policy.Manager
}
func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
validator := account.NewValidator()
for _, user := range config.Users {
u, err := user.ToMemoryUser()
if err != nil {
return nil, errors.New("failed to get hysteria user").Base(err).AtError()
}
if err := validator.Add(u); err != nil {
return nil, errors.New("failed to add user").Base(err).AtError()
}
}
v := core.MustFromContext(ctx)
s := &Server{
config: config,
validator: validator,
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
}
return s, nil
}
func (s *Server) HysteriaInboundValidator() *account.Validator {
return s.validator
}
func (s *Server) AddUser(ctx context.Context, u *protocol.MemoryUser) error {
return s.validator.Add(u)
}
func (s *Server) RemoveUser(ctx context.Context, e string) error {
return s.validator.Del(e)
}
func (s *Server) GetUser(ctx context.Context, email string) *protocol.MemoryUser {
return s.validator.GetByEmail(email)
}
func (s *Server) GetUsers(ctx context.Context) []*protocol.MemoryUser {
return s.validator.GetAll()
}
func (s *Server) GetUsersCount(context.Context) int64 {
return s.validator.GetCount()
}
func (s *Server) Network() []net.Network {
return []net.Network{net.Network_TCP}
}
func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error {
inbound := session.InboundFromContext(ctx)
inbound.Name = "hysteria"
inbound.CanSpliceCopy = 3
var useremail string
var userlevel uint32
type User interface{ User() *protocol.MemoryUser }
if v, ok := conn.(User); ok {
inbound.User = v.User()
if inbound.User != nil {
useremail = inbound.User.Email
userlevel = inbound.User.Level
}
}
iConn := stat.TryUnwrapStatsConn(conn)
if _, ok := iConn.(*hysteria.InterUdpConn); ok {
r := io.Reader(conn)
b := make([]byte, MaxUDPSize)
df := &Defragger{}
var firstMsg *UDPMessage
var firstDest net.Destination
for {
n, err := r.Read(b)
if err != nil {
return err
}
msg, err := ParseUDPMessage(b[:n])
if err != nil {
continue
}
dfMsg := df.Feed(msg)
if dfMsg == nil {
continue
}
firstMsg = dfMsg
firstDest, err = net.ParseDestination("udp:" + firstMsg.Addr)
if err != nil {
errors.LogDebug(context.Background(), dfMsg.Addr, " ParseDestination err ", err)
continue
}
break
}
reader := &UDPReader{
Reader: r,
buf: b,
df: df,
firstMsg: firstMsg,
firstDest: &firstDest,
}
writer := &UDPWriter{
Writer: conn,
buf: make([]byte, MaxUDPSize),
addr: firstMsg.Addr,
}
return dispatcher.DispatchLink(ctx, firstDest, &transport.Link{
Reader: reader,
Writer: writer,
})
} else {
sessionPolicy := s.policyManager.ForLevel(userlevel)
common.Must(conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)))
addr, err := ReadTCPRequest(conn)
if err != nil {
log.Record(&log.AccessMessage{
From: conn.RemoteAddr(),
To: "",
Status: log.AccessRejected,
Reason: err,
})
return errors.New("failed to create request from: ", conn.RemoteAddr()).Base(err)
}
common.Must(conn.SetReadDeadline(time.Time{}))
dest, err := net.ParseDestination("tcp:" + addr)
if err != nil {
return err
}
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
From: conn.RemoteAddr(),
To: dest,
Status: log.AccessAccepted,
Reason: "",
Email: useremail,
})
errors.LogInfo(ctx, "tunnelling request to ", dest)
bufferedWriter := buf.NewBufferedWriter(buf.NewWriter(conn))
err = WriteTCPResponse(bufferedWriter, true, "")
if err != nil {
return errors.New("failed to write response").Base(err)
}
if err := bufferedWriter.SetBuffered(false); err != nil {
return err
}
return dispatcher.DispatchLink(ctx, dest, &transport.Link{
Reader: buf.NewReader(conn),
Writer: bufferedWriter,
})
}
}
func init() {
common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return NewServer(ctx, config.(*ServerConfig))
}))
}
-53
View File
@@ -1,53 +0,0 @@
//go:build linux
package brutal
import (
"os"
"unsafe"
"github.com/xtls/xray-core/common/net"
"golang.org/x/sys/unix"
)
//go:linkname setsockopt syscall.setsockopt
func setsockopt(s int, level int, name int, val unsafe.Pointer, vallen uintptr) (err error)
const (
TCP_BRUTAL_PARAMS = 23301
)
type TCPBrutalParams struct {
Rate uint64
CwndGain uint32
}
func setBrutalFD(fd uintptr, sendBPS uint64) error {
err := unix.SetsockoptString(int(fd), unix.IPPROTO_TCP, unix.TCP_CONGESTION, "brutal")
if err != nil {
return err
}
params := TCPBrutalParams{
Rate: sendBPS,
CwndGain: 20, // hysteria2 default
}
err = setsockopt(int(fd), unix.IPPROTO_TCP, TCP_BRUTAL_PARAMS, unsafe.Pointer(&params), unsafe.Sizeof(params))
if err != nil {
return os.NewSyscallError("setsockopt IPPROTO_TCP TCP_BRUTAL_PARAMS", err)
}
return nil
}
func SetBrutal(conn *net.TCPConn, sendBPS uint64) error {
syscallConn, err := conn.SyscallConn()
if err != nil {
return err
}
syscallConn.Control(func(fd uintptr) {
err = setBrutalFD(fd, sendBPS)
})
if err != nil {
return err
}
return nil
}
@@ -1,12 +0,0 @@
//go:build !linux
package brutal
import (
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
)
func SetBrutal(conn *net.TCPConn, sendBPS uint64) error {
return errors.New("brutal not available on this platform")
}
+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")
+9 -13
View File
@@ -31,18 +31,7 @@ func (s *server) Addr() net.Addr {
return nil
}
func (s *server) Handle(conn net.Conn) {
upgradedConn, err := s.upgrade(conn)
if err != nil {
common.CloseIfExists(conn)
errors.LogInfoInner(context.Background(), err, "failed to handle request")
return
}
s.addConn(upgradedConn)
}
// upgrade execute a fake websocket upgrade process and return the available connection
func (s *server) upgrade(conn net.Conn) (stat.Connection, error) {
func (s *server) Handle(conn net.Conn) (stat.Connection, error) {
connReader := bufio.NewReader(conn)
req, err := http.ReadRequest(connReader)
if err != nil {
@@ -63,6 +52,7 @@ func (s *server) upgrade(conn net.Conn) (stat.Connection, error) {
connection := strings.ToLower(req.Header.Get("Connection"))
upgrade := strings.ToLower(req.Header.Get("Upgrade"))
if connection != "upgrade" || upgrade != "websocket" {
_ = conn.Close()
return nil, errors.New("unrecognized request")
}
resp := &http.Response{
@@ -77,6 +67,7 @@ func (s *server) upgrade(conn net.Conn) (stat.Connection, error) {
resp.Header.Set("Upgrade", "websocket")
err = resp.Write(conn)
if err != nil {
_ = conn.Close()
return nil, err
}
@@ -108,7 +99,12 @@ func (s *server) keepAccepting() {
if err != nil {
return
}
go s.Handle(conn)
handledConn, err := s.Handle(conn)
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to handle request")
continue
}
s.addConn(handledConn)
}
}
+2 -8
View File
@@ -1,8 +1,6 @@
package hysteria
import (
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria/padding"
@@ -25,15 +23,11 @@ const (
StatusAuthOK = 233
udpMessageChanSize = 1024
FrameTypeTCPRequest = 0x401
idleCleanupInterval = 1 * time.Second
)
var (
authRequestPadding = padding.Padding{Min: 256, Max: 2048}
authResponsePadding = padding.Padding{Min: 256, Max: 2048}
authRequestPadding = padding.Padding{Min: 256, Max: 2048}
// authResponsePadding = padding.Padding{Min: 256, Max: 2048}
)
type Status int
+9 -105
View File
@@ -38,16 +38,6 @@ type Config struct {
MaxIdleTimeout int64 `protobuf:"varint,13,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"`
KeepAlivePeriod int64 `protobuf:"varint,14,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"`
DisablePathMtuDiscovery bool `protobuf:"varint,15,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"`
MaxIncomingStreams int64 `protobuf:"varint,16,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"`
UdpIdleTimeout int64 `protobuf:"varint,17,opt,name=udp_idle_timeout,json=udpIdleTimeout,proto3" json:"udp_idle_timeout,omitempty"`
MasqType string `protobuf:"bytes,18,opt,name=masq_type,json=masqType,proto3" json:"masq_type,omitempty"`
MasqFile string `protobuf:"bytes,19,opt,name=masq_file,json=masqFile,proto3" json:"masq_file,omitempty"`
MasqUrl string `protobuf:"bytes,20,opt,name=masq_url,json=masqUrl,proto3" json:"masq_url,omitempty"`
MasqUrlRewriteHost bool `protobuf:"varint,21,opt,name=masq_url_rewrite_host,json=masqUrlRewriteHost,proto3" json:"masq_url_rewrite_host,omitempty"`
MasqUrlInsecure bool `protobuf:"varint,22,opt,name=masq_url_insecure,json=masqUrlInsecure,proto3" json:"masq_url_insecure,omitempty"`
MasqString string `protobuf:"bytes,23,opt,name=masq_string,json=masqString,proto3" json:"masq_string,omitempty"`
MasqStringHeaders map[string]string `protobuf:"bytes,24,rep,name=masq_string_headers,json=masqStringHeaders,proto3" json:"masq_string_headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
MasqStringStatusCode int32 `protobuf:"varint,25,opt,name=masq_string_status_code,json=masqStringStatusCode,proto3" json:"masq_string_status_code,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -187,81 +177,11 @@ func (x *Config) GetDisablePathMtuDiscovery() bool {
return false
}
func (x *Config) GetMaxIncomingStreams() int64 {
if x != nil {
return x.MaxIncomingStreams
}
return 0
}
func (x *Config) GetUdpIdleTimeout() int64 {
if x != nil {
return x.UdpIdleTimeout
}
return 0
}
func (x *Config) GetMasqType() string {
if x != nil {
return x.MasqType
}
return ""
}
func (x *Config) GetMasqFile() string {
if x != nil {
return x.MasqFile
}
return ""
}
func (x *Config) GetMasqUrl() string {
if x != nil {
return x.MasqUrl
}
return ""
}
func (x *Config) GetMasqUrlRewriteHost() bool {
if x != nil {
return x.MasqUrlRewriteHost
}
return false
}
func (x *Config) GetMasqUrlInsecure() bool {
if x != nil {
return x.MasqUrlInsecure
}
return false
}
func (x *Config) GetMasqString() string {
if x != nil {
return x.MasqString
}
return ""
}
func (x *Config) GetMasqStringHeaders() map[string]string {
if x != nil {
return x.MasqStringHeaders
}
return nil
}
func (x *Config) GetMasqStringStatusCode() int32 {
if x != nil {
return x.MasqStringStatusCode
}
return 0
}
var File_transport_internet_hysteria_config_proto protoreflect.FileDescriptor
const file_transport_internet_hysteria_config_proto_rawDesc = "" +
"\n" +
"(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\xf0\b\n" +
"(transport/internet/hysteria/config.proto\x12 xray.transport.internet.hysteria\"\xd1\x04\n" +
"\x06Config\x12\x18\n" +
"\aversion\x18\x01 \x01(\x05R\aversion\x12\x12\n" +
"\x04auth\x18\x02 \x01(\tR\x04auth\x12\x1e\n" +
@@ -280,21 +200,7 @@ const file_transport_internet_hysteria_config_proto_rawDesc = "" +
"\x17max_conn_receive_window\x18\f \x01(\x04R\x14maxConnReceiveWindow\x12(\n" +
"\x10max_idle_timeout\x18\r \x01(\x03R\x0emaxIdleTimeout\x12*\n" +
"\x11keep_alive_period\x18\x0e \x01(\x03R\x0fkeepAlivePeriod\x12;\n" +
"\x1adisable_path_mtu_discovery\x18\x0f \x01(\bR\x17disablePathMtuDiscovery\x120\n" +
"\x14max_incoming_streams\x18\x10 \x01(\x03R\x12maxIncomingStreams\x12(\n" +
"\x10udp_idle_timeout\x18\x11 \x01(\x03R\x0eudpIdleTimeout\x12\x1b\n" +
"\tmasq_type\x18\x12 \x01(\tR\bmasqType\x12\x1b\n" +
"\tmasq_file\x18\x13 \x01(\tR\bmasqFile\x12\x19\n" +
"\bmasq_url\x18\x14 \x01(\tR\amasqUrl\x121\n" +
"\x15masq_url_rewrite_host\x18\x15 \x01(\bR\x12masqUrlRewriteHost\x12*\n" +
"\x11masq_url_insecure\x18\x16 \x01(\bR\x0fmasqUrlInsecure\x12\x1f\n" +
"\vmasq_string\x18\x17 \x01(\tR\n" +
"masqString\x12o\n" +
"\x13masq_string_headers\x18\x18 \x03(\v2?.xray.transport.internet.hysteria.Config.MasqStringHeadersEntryR\x11masqStringHeaders\x125\n" +
"\x17masq_string_status_code\x18\x19 \x01(\x05R\x14masqStringStatusCode\x1aD\n" +
"\x16MasqStringHeadersEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x82\x01\n" +
"\x1adisable_path_mtu_discovery\x18\x0f \x01(\bR\x17disablePathMtuDiscoveryB\x82\x01\n" +
"$com.xray.transport.internet.hysteriaP\x01Z5github.com/xtls/xray-core/transport/internet/hysteria\xaa\x02 Xray.Transport.Internet.Hysteriab\x06proto3"
var (
@@ -309,18 +215,16 @@ func file_transport_internet_hysteria_config_proto_rawDescGZIP() []byte {
return file_transport_internet_hysteria_config_proto_rawDescData
}
var file_transport_internet_hysteria_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_transport_internet_hysteria_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_transport_internet_hysteria_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.transport.internet.hysteria.Config
nil, // 1: xray.transport.internet.hysteria.Config.MasqStringHeadersEntry
}
var file_transport_internet_hysteria_config_proto_depIdxs = []int32{
1, // 0: xray.transport.internet.hysteria.Config.masq_string_headers:type_name -> xray.transport.internet.hysteria.Config.MasqStringHeadersEntry
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
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_transport_internet_hysteria_config_proto_init() }
@@ -334,7 +238,7 @@ func file_transport_internet_hysteria_config_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_hysteria_config_proto_rawDesc), len(file_transport_internet_hysteria_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
+1 -11
View File
@@ -23,15 +23,5 @@ message Config {
int64 max_idle_timeout = 13;
int64 keep_alive_period = 14;
bool disable_path_mtu_discovery = 15;
int64 max_incoming_streams = 16;
}
int64 udp_idle_timeout = 17;
string masq_type = 18;
string masq_file = 19;
string masq_url = 20;
bool masq_url_rewrite_host = 21;
bool masq_url_insecure = 22;
string masq_string = 23;
map<string, string> masq_string_headers = 24;
int32 masq_string_status_code = 25;
}
+2 -58
View File
@@ -3,28 +3,16 @@ package hysteria
import (
"encoding/binary"
"io"
"sync"
"time"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/quicvarint"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
)
type interConn struct {
stream *quic.Stream
local net.Addr
remote net.Addr
client bool
mutex sync.Mutex
user *protocol.MemoryUser
}
func (i *interConn) User() *protocol.MemoryUser {
return i.user
}
func (i *interConn) Read(b []byte) (int, error) {
@@ -32,22 +20,6 @@ func (i *interConn) Read(b []byte) (int, error) {
}
func (i *interConn) Write(b []byte) (int, error) {
if i.client {
i.mutex.Lock()
if i.client {
buf := make([]byte, 0, quicvarint.Len(FrameTypeTCPRequest)+len(b))
buf = quicvarint.Append(buf, FrameTypeTCPRequest)
buf = append(buf, b...)
_, err := i.stream.Write(buf)
if err != nil {
return 0, err
}
i.client = false
return len(b), nil
}
i.mutex.Unlock()
}
return i.stream.Write(b)
}
@@ -81,34 +53,10 @@ type InterUdpConn struct {
local net.Addr
remote net.Addr
id uint32
ch chan []byte
id uint32
ch chan []byte
closed bool
closeFunc func()
last time.Time
mutex sync.Mutex
user *protocol.MemoryUser
}
func (i *InterUdpConn) User() *protocol.MemoryUser {
return i.user
}
func (i *InterUdpConn) SetLast() {
i.mutex.Lock()
defer i.mutex.Unlock()
i.last = time.Now()
}
func (i *InterUdpConn) GetLast() time.Time {
i.mutex.Lock()
defer i.mutex.Unlock()
return i.last
}
func (i *InterUdpConn) Read(p []byte) (int, error) {
@@ -120,14 +68,10 @@ func (i *InterUdpConn) Read(p []byte) (int, error) {
if n != len(b) {
return 0, io.ErrShortBuffer
}
i.SetLast()
return n, nil
}
func (i *InterUdpConn) Write(p []byte) (int, error) {
i.SetLast()
binary.BigEndian.PutUint32(p, i.id)
if err := i.conn.SendDatagram(p); err != nil {
return 0, err
+24 -27
View File
@@ -26,23 +26,15 @@ import (
"github.com/xtls/xray-core/transport/internet/tls"
)
type udpSessionManagerClient struct {
type udpSessionManager struct {
conn *quic.Conn
m map[uint32]*InterUdpConn
next uint32
nextId uint32
closed bool
mutex sync.RWMutex
}
func (m *udpSessionManagerClient) close(udpConn *InterUdpConn) {
if !udpConn.closed {
udpConn.closed = true
close(udpConn.ch)
delete(m.m, udpConn.id)
}
}
func (m *udpSessionManagerClient) run() {
func (m *udpSessionManager) run() {
for {
d, err := m.conn.ReceiveDatagram(context.Background())
if err != nil {
@@ -52,22 +44,29 @@ func (m *udpSessionManagerClient) run() {
if len(d) < 4 {
continue
}
id := binary.BigEndian.Uint32(d[:4])
sessionId := binary.BigEndian.Uint32(d[:4])
m.feed(id, d)
m.feed(sessionId, d)
}
m.mutex.Lock()
defer m.mutex.Unlock()
m.closed = true
for _, udpConn := range m.m {
m.close(udpConn)
}
}
func (m *udpSessionManagerClient) udp() (*InterUdpConn, error) {
func (m *udpSessionManager) close(udpConn *InterUdpConn) {
if !udpConn.closed {
udpConn.closed = true
close(udpConn.ch)
delete(m.m, udpConn.id)
}
}
func (m *udpSessionManager) udp() (*InterUdpConn, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
@@ -80,7 +79,7 @@ func (m *udpSessionManagerClient) udp() (*InterUdpConn, error) {
local: m.conn.LocalAddr(),
remote: m.conn.RemoteAddr(),
id: m.next,
id: m.nextId,
ch: make(chan []byte, udpMessageChanSize),
}
udpConn.closeFunc = func() {
@@ -88,17 +87,17 @@ func (m *udpSessionManagerClient) udp() (*InterUdpConn, error) {
defer m.mutex.Unlock()
m.close(udpConn)
}
m.m[m.next] = udpConn
m.next++
m.m[m.nextId] = udpConn
m.nextId++
return udpConn, nil
}
func (m *udpSessionManagerClient) feed(id uint32, d []byte) {
func (m *udpSessionManager) feed(sessionId uint32, d []byte) {
m.mutex.RLock()
defer m.mutex.RUnlock()
udpConn, ok := m.m[id]
udpConn, ok := m.m[sessionId]
if !ok {
return
}
@@ -118,7 +117,7 @@ type client struct {
tlsConfig *go_tls.Config
socketConfig *internet.SocketConfig
udpmaskManager *finalmask.UdpmaskManager
udpSM *udpSessionManagerClient
udpSM *udpSessionManager
mutex sync.Mutex
}
@@ -270,10 +269,10 @@ func (c *client) dial() error {
c.pktConn = pktConn
c.conn = quicConn
if serverUdp {
c.udpSM = &udpSessionManagerClient{
conn: quicConn,
m: make(map[uint32]*InterUdpConn),
next: 1,
c.udpSM = &udpSessionManager{
conn: quicConn,
m: make(map[uint32]*InterUdpConn),
nextId: 1,
}
go c.udpSM.run()
}
@@ -308,8 +307,6 @@ func (c *client) tcp() (stat.Connection, error) {
stream: stream,
local: c.conn.LocalAddr(),
remote: c.conn.RemoteAddr(),
client: true,
}, nil
}
-412
View File
@@ -1,412 +0,0 @@
package hysteria
import (
"context"
gotls "crypto/tls"
"encoding/binary"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/apernet/quic-go"
"github.com/apernet/quic-go/http3"
"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/protocol"
"github.com/xtls/xray-core/proxy/hysteria/account"
hyCtx "github.com/xtls/xray-core/proxy/hysteria/ctx"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/hysteria/congestion"
"github.com/xtls/xray-core/transport/internet/tls"
)
type udpSessionManagerServer struct {
conn *quic.Conn
m map[uint32]*InterUdpConn
addConn internet.ConnHandler
stopCh chan struct{}
udpIdleTimeout time.Duration
mutex sync.RWMutex
user *protocol.MemoryUser
}
func (m *udpSessionManagerServer) close(udpConn *InterUdpConn) {
if !udpConn.closed {
udpConn.closed = true
close(udpConn.ch)
delete(m.m, udpConn.id)
}
}
func (m *udpSessionManagerServer) clean() {
ticker := time.NewTicker(idleCleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.mutex.RLock()
now := time.Now()
timeoutConn := make([]*InterUdpConn, 0, len(m.m))
for _, udpConn := range m.m {
if now.Sub(udpConn.GetLast()) > m.udpIdleTimeout {
timeoutConn = append(timeoutConn, udpConn)
}
}
m.mutex.RUnlock()
for _, udpConn := range timeoutConn {
m.mutex.Lock()
m.close(udpConn)
m.mutex.Unlock()
}
case <-m.stopCh:
return
}
}
}
func (m *udpSessionManagerServer) run() {
for {
d, err := m.conn.ReceiveDatagram(context.Background())
if err != nil {
break
}
if len(d) < 4 {
continue
}
id := binary.BigEndian.Uint32(d[:4])
m.feed(id, d)
}
m.mutex.Lock()
defer m.mutex.Unlock()
close(m.stopCh)
for _, udpConn := range m.m {
m.close(udpConn)
}
}
func (m *udpSessionManagerServer) feed(id uint32, d []byte) {
m.mutex.RLock()
udpConn, ok := m.m[id]
m.mutex.RUnlock()
if !ok {
m.mutex.Lock()
udpConn, ok = m.m[id]
if !ok {
udpConn = &InterUdpConn{
conn: m.conn,
local: m.conn.LocalAddr(),
remote: m.conn.RemoteAddr(),
id: id,
ch: make(chan []byte, udpMessageChanSize),
last: time.Now(),
user: m.user,
}
udpConn.closeFunc = func() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.close(udpConn)
}
m.m[id] = udpConn
m.addConn(udpConn)
}
m.mutex.Unlock()
}
select {
case udpConn.ch <- d:
default:
}
}
type httpHandler struct {
ctx context.Context
conn *quic.Conn
addConn internet.ConnHandler
config *Config
validator *account.Validator
masqHandler http.Handler
auth bool
mutex sync.Mutex
user *protocol.MemoryUser
}
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost && r.Host == URLHost && r.URL.Path == URLPath {
h.mutex.Lock()
defer h.mutex.Unlock()
if h.auth {
w.Header().Set(ResponseHeaderUDPEnabled, strconv.FormatBool(hyCtx.RequireDatagramFromContext(h.ctx)))
w.Header().Set(CommonHeaderCCRX, strconv.FormatUint(h.config.Down, 10))
w.Header().Set(CommonHeaderPadding, authResponsePadding.String())
w.WriteHeader(StatusAuthOK)
return
}
auth := r.Header.Get(RequestHeaderAuth)
clientDown, _ := strconv.ParseUint(r.Header.Get(CommonHeaderCCRX), 10, 64)
var user *protocol.MemoryUser
var ok bool
if h.validator != nil {
user = h.validator.Get(auth)
} else if auth == h.config.Auth {
ok = true
}
if user != nil || ok {
h.auth = true
h.user = user
switch h.config.Congestion {
case "reno":
errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion reno")
case "bbr":
errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion bbr")
congestion.UseBBR(h.conn)
case "brutal", "":
if h.config.Up == 0 || clientDown == 0 {
errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion bbr")
congestion.UseBBR(h.conn)
} else {
errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion brutal bytes per second ", min(h.config.Up, clientDown))
congestion.UseBrutal(h.conn, min(h.config.Up, clientDown))
}
case "force-brutal":
errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion brutal bytes per second ", h.config.Up)
congestion.UseBrutal(h.conn, h.config.Up)
default:
errors.LogDebug(context.Background(), h.conn.RemoteAddr(), " ", "congestion reno")
}
if hyCtx.RequireDatagramFromContext(h.ctx) {
udpSM := &udpSessionManagerServer{
conn: h.conn,
m: make(map[uint32]*InterUdpConn),
addConn: h.addConn,
stopCh: make(chan struct{}),
udpIdleTimeout: time.Duration(h.config.UdpIdleTimeout) * time.Second,
user: h.user,
}
go udpSM.clean()
go udpSM.run()
}
w.Header().Set(ResponseHeaderUDPEnabled, strconv.FormatBool(hyCtx.RequireDatagramFromContext(h.ctx)))
w.Header().Set(CommonHeaderCCRX, strconv.FormatUint(h.config.Down, 10))
w.Header().Set(CommonHeaderPadding, authResponsePadding.String())
w.WriteHeader(StatusAuthOK)
return
}
}
h.masqHandler.ServeHTTP(w, r)
}
func (h *httpHandler) ProxyStreamHijacker(ft http3.FrameType, id quic.ConnectionTracingID, stream *quic.Stream, err error) (bool, error) {
if err != nil || !h.auth {
return false, nil
}
switch ft {
case FrameTypeTCPRequest:
h.addConn(&interConn{
stream: stream,
local: h.conn.LocalAddr(),
remote: h.conn.RemoteAddr(),
user: h.user,
})
return true, nil
default:
return false, nil
}
}
type Listener struct {
ctx context.Context
pktConn net.PacketConn
listener *quic.Listener
addConn internet.ConnHandler
config *Config
validator *account.Validator
masqHandler http.Handler
}
func (l *Listener) handleClient(conn *quic.Conn) {
handler := &httpHandler{
ctx: l.ctx,
conn: conn,
addConn: l.addConn,
config: l.config,
validator: l.validator,
masqHandler: l.masqHandler,
}
h3 := http3.Server{
Handler: handler,
StreamHijacker: handler.ProxyStreamHijacker,
}
err := h3.ServeQUICConn(conn)
errors.LogDebug(context.Background(), conn.RemoteAddr(), " disconnected with err ", err)
_ = conn.CloseWithError(closeErrCodeOK, "")
}
func (l *Listener) keepAccepting() {
for {
conn, err := l.listener.Accept(context.Background())
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to accept QUIC connection")
break
}
go l.handleClient(conn)
}
}
func (l *Listener) Addr() net.Addr {
return l.listener.Addr()
}
func (l *Listener) Close() error {
err := l.listener.Close()
_ = l.pktConn.Close()
return err
}
func Listen(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) {
if address.Family().IsDomain() {
return nil, errors.New("address is domain")
}
tlsConfig := tls.ConfigFromStreamSettings(streamSettings)
if tlsConfig == nil {
return nil, errors.New("tls config is nil")
}
config := streamSettings.ProtocolSettings.(*Config)
validator := hyCtx.ValidatorFromContext(ctx)
if config.Auth == "" && validator == nil {
return nil, errors.New("validator is nil")
}
var masqHandler http.Handler
switch strings.ToLower(config.MasqType) {
case "", "404":
masqHandler = http.NotFoundHandler()
case "file":
masqHandler = http.FileServer(http.Dir(config.MasqFile))
case "proxy":
u, err := url.Parse(config.MasqUrl)
if err != nil {
return nil, err
}
transport := http.DefaultTransport.(*http.Transport)
if config.MasqUrlInsecure {
transport = transport.Clone()
transport.TLSClientConfig = &gotls.Config{
InsecureSkipVerify: true,
}
}
masqHandler = &httputil.ReverseProxy{
Rewrite: func(pr *httputil.ProxyRequest) {
pr.SetURL(u)
if !config.MasqUrlRewriteHost {
pr.Out.Host = pr.In.Host
}
},
Transport: transport,
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
w.WriteHeader(http.StatusBadGateway)
},
}
case "string":
masqHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for k, v := range config.MasqStringHeaders {
w.Header().Set(k, v)
}
if config.MasqStringStatusCode != 0 {
w.WriteHeader(int(config.MasqStringStatusCode))
} else {
w.WriteHeader(http.StatusOK)
}
_, _ = w.Write([]byte(config.MasqString))
})
default:
return nil, errors.New("unknown masq type")
}
raw, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{IP: address.IP(), Port: int(port)}, streamSettings.SocketSettings)
if err != nil {
return nil, err
}
var pktConn net.PacketConn
pktConn = raw
if streamSettings.UdpmaskManager != nil {
pktConn, err = streamSettings.UdpmaskManager.WrapPacketConnServer(raw)
if err != nil {
raw.Close()
return nil, errors.New("mask err").Base(err)
}
}
quicConfig := &quic.Config{
InitialStreamReceiveWindow: config.InitStreamReceiveWindow,
MaxStreamReceiveWindow: config.MaxStreamReceiveWindow,
InitialConnectionReceiveWindow: config.InitConnReceiveWindow,
MaxConnectionReceiveWindow: config.MaxConnReceiveWindow,
MaxIdleTimeout: time.Duration(config.MaxIdleTimeout) * time.Second,
MaxIncomingStreams: config.MaxIncomingStreams,
DisablePathMTUDiscovery: config.DisablePathMtuDiscovery,
EnableDatagrams: true,
MaxDatagramFrameSize: MaxDatagramFrameSize,
DisablePathManager: true,
}
qListener, err := quic.Listen(pktConn, tlsConfig.GetTLSConfig(), quicConfig)
if err != nil {
_ = pktConn.Close()
return nil, err
}
listener := &Listener{
ctx: ctx,
pktConn: pktConn,
listener: qListener,
addConn: handler,
config: config,
validator: validator,
masqHandler: masqHandler,
}
go listener.keepAccepting()
return listener, nil
}
func init() {
common.Must(internet.RegisterTransportListener(protocolName, Listen))
}
+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
}
+10 -14
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)
@@ -524,13 +527,11 @@ func ConfigFromStreamSettings(settings *internet.MemoryStreamConfig) *Config {
func ParseCurveName(curveNames []string) []tls.CurveID {
curveMap := map[string]tls.CurveID{
"curvep256": tls.CurveP256,
"curvep384": tls.CurveP384,
"curvep521": tls.CurveP521,
"x25519": tls.X25519,
"x25519mlkem768": tls.X25519MLKEM768,
"secp256r1mlkem768": tls.SecP256r1MLKEM768,
"secp384r1mlkem1024": tls.SecP384r1MLKEM1024,
"curvep256": tls.CurveP256,
"curvep384": tls.CurveP384,
"curvep521": tls.CurveP521,
"x25519": tls.X25519,
"x25519mlkem768": tls.X25519MLKEM768,
}
var curveIDs []tls.CurveID
@@ -557,19 +558,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
}
}
}
+70 -1
View File
@@ -3,6 +3,8 @@ package tls
import (
"bytes"
"context"
"crypto/ecdh"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"encoding/binary"
@@ -21,6 +23,8 @@ import (
"golang.org/x/net/http2"
"github.com/miekg/dns"
"github.com/xtls/reality"
"github.com/xtls/reality/hpke"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/utils"
@@ -253,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)
@@ -326,6 +329,34 @@ func dnsQuery(server string, domain string, sockopt *internet.SocketConfig) ([]b
return nil, dns2.DefaultTTL, nil
}
// reference github.com/OmarTariq612/goech
func MarshalBinary(ech reality.EchConfig) ([]byte, error) {
var b cryptobyte.Builder
b.AddUint16(ech.Version)
b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
child.AddUint8(ech.ConfigID)
child.AddUint16(ech.KemID)
child.AddUint16(uint16(len(ech.PublicKey)))
child.AddBytes(ech.PublicKey)
child.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
for _, cipherSuite := range ech.SymmetricCipherSuite {
child.AddUint16(cipherSuite.KDFID)
child.AddUint16(cipherSuite.AEADID)
}
})
child.AddUint8(ech.MaxNameLength)
child.AddUint8(uint8(len(ech.PublicName)))
child.AddBytes(ech.PublicName)
child.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) {
for _, extention := range ech.Extensions {
child.AddUint16(extention.Type)
child.AddBytes(extention.Data)
}
})
})
return b.Bytes()
}
var ErrInvalidLen = errors.New("goech: invalid length")
func ConvertToGoECHKeys(data []byte) ([]tls.EncryptedClientHelloKey, error) {
@@ -360,3 +391,41 @@ func ConvertToGoECHKeys(data []byte) ([]tls.EncryptedClientHelloKey, error) {
}
return keys, nil
}
const ExtensionEncryptedClientHello = 0xfe0d
const KDF_HKDF_SHA384 = 0x0002
const KDF_HKDF_SHA512 = 0x0003
func GenerateECHKeySet(configID uint8, domain string, kem uint16) (reality.EchConfig, []byte, error) {
config := reality.EchConfig{
Version: ExtensionEncryptedClientHello,
ConfigID: configID,
PublicName: []byte(domain),
KemID: kem,
SymmetricCipherSuite: []reality.EchCipher{
{KDFID: hpke.KDF_HKDF_SHA256, AEADID: hpke.AEAD_AES_128_GCM},
{KDFID: hpke.KDF_HKDF_SHA256, AEADID: hpke.AEAD_AES_256_GCM},
{KDFID: hpke.KDF_HKDF_SHA256, AEADID: hpke.AEAD_ChaCha20Poly1305},
{KDFID: KDF_HKDF_SHA384, AEADID: hpke.AEAD_AES_128_GCM},
{KDFID: KDF_HKDF_SHA384, AEADID: hpke.AEAD_AES_256_GCM},
{KDFID: KDF_HKDF_SHA384, AEADID: hpke.AEAD_ChaCha20Poly1305},
{KDFID: KDF_HKDF_SHA512, AEADID: hpke.AEAD_AES_128_GCM},
{KDFID: KDF_HKDF_SHA512, AEADID: hpke.AEAD_AES_256_GCM},
{KDFID: KDF_HKDF_SHA512, AEADID: hpke.AEAD_ChaCha20Poly1305},
},
MaxNameLength: 0,
Extensions: nil,
}
// if kem == hpke.DHKEM_X25519_HKDF_SHA256 {
curve := ecdh.X25519()
priv := make([]byte, 32) //x25519
_, err := io.ReadFull(rand.Reader, priv)
if err != nil {
return config, nil, err
}
privKey, _ := curve.NewPrivateKey(priv)
config.PublicKey = privKey.PublicKey().Bytes()
return config, priv, nil
// }
// TODO: add mlkem768 (former kyber768 draft00). The golang mlkem private key is 64 bytes seed?
}
+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
@@ -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
}
+140
View File
@@ -0,0 +1,140 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.33.5
// source: transport/internet/xdrive/config.proto
package xdrive
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
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 `protogen:"open.v1"`
RemoteFolder string `protobuf:"bytes,1,opt,name=remote_folder,json=remoteFolder,proto3" json:"remote_folder,omitempty"`
Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"`
Secrets []string `protobuf:"bytes,3,rep,name=secrets,proto3" json:"secrets,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_transport_internet_xdrive_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_transport_internet_xdrive_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_transport_internet_xdrive_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetRemoteFolder() string {
if x != nil {
return x.RemoteFolder
}
return ""
}
func (x *Config) GetService() string {
if x != nil {
return x.Service
}
return ""
}
func (x *Config) GetSecrets() []string {
if x != nil {
return x.Secrets
}
return nil
}
var File_transport_internet_xdrive_config_proto protoreflect.FileDescriptor
const file_transport_internet_xdrive_config_proto_rawDesc = "" +
"\n" +
"&transport/internet/xdrive/config.proto\x12\x1exray.transport.internet.xdrive\"a\n" +
"\x06Config\x12#\n" +
"\rremote_folder\x18\x01 \x01(\tR\fremoteFolder\x12\x18\n" +
"\aservice\x18\x02 \x01(\tR\aservice\x12\x18\n" +
"\asecrets\x18\x03 \x03(\tR\asecretsB5Z3github.com/xtls/xray-core/transport/internet/xdriveb\x06proto3"
var (
file_transport_internet_xdrive_config_proto_rawDescOnce sync.Once
file_transport_internet_xdrive_config_proto_rawDescData []byte
)
func file_transport_internet_xdrive_config_proto_rawDescGZIP() []byte {
file_transport_internet_xdrive_config_proto_rawDescOnce.Do(func() {
file_transport_internet_xdrive_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_xdrive_config_proto_rawDesc), len(file_transport_internet_xdrive_config_proto_rawDesc)))
})
return file_transport_internet_xdrive_config_proto_rawDescData
}
var file_transport_internet_xdrive_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_transport_internet_xdrive_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.transport.internet.xdrive.Config
}
var file_transport_internet_xdrive_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_transport_internet_xdrive_config_proto_init() }
func file_transport_internet_xdrive_config_proto_init() {
if File_transport_internet_xdrive_config_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_xdrive_config_proto_rawDesc), len(file_transport_internet_xdrive_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_transport_internet_xdrive_config_proto_goTypes,
DependencyIndexes: file_transport_internet_xdrive_config_proto_depIdxs,
MessageInfos: file_transport_internet_xdrive_config_proto_msgTypes,
}.Build()
File_transport_internet_xdrive_config_proto = out.File
file_transport_internet_xdrive_config_proto_goTypes = nil
file_transport_internet_xdrive_config_proto_depIdxs = nil
}
+10
View File
@@ -0,0 +1,10 @@
syntax = "proto3";
package xray.transport.internet.xdrive;
option go_package = "github.com/xtls/xray-core/transport/internet/xdrive";
message Config {
string remote_folder = 1;
string service = 2;
repeated string secrets = 3;
}
+594
View File
@@ -0,0 +1,594 @@
package xdrive
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
"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/signal/done"
"github.com/xtls/xray-core/common/uuid"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
)
const protocolName = "xdrive"
// Configuration constants
const (
// ReadPollInterval is the interval for polling when reading data
ReadPollInterval = 100 * time.Millisecond
// ServerPollInterval is the interval for polling new connections on server
ServerPollInterval = 500 * time.Millisecond
// ReadTimeout is the timeout for read operations
ReadTimeout = 10 * time.Second
// RetentionWindow is how long files are kept before cleanup
RetentionWindow = 10 * time.Second
// CleanupInterval is how often the cleanup routine runs
CleanupInterval = 5 * time.Second
)
// DriveService defines the interface for remote storage services
type DriveService interface {
// Login authenticates with the service (no-op for local)
Login(ctx context.Context) error
// Upload creates a file with the given name and content
Upload(ctx context.Context, name string, data []byte) error
// List returns files matching the prefix, created within the given duration
List(ctx context.Context, prefix string, within time.Duration) ([]FileInfo, error)
// Download retrieves the content of a file
Download(ctx context.Context, name string) ([]byte, error)
// Delete removes a file
Delete(ctx context.Context, name string) error
}
// FileInfo represents information about a file
type FileInfo struct {
Name string
CreatedAt time.Time
}
// LocalDriveService implements DriveService using the local filesystem
type LocalDriveService struct {
remoteFolder string
mu sync.RWMutex
}
// NewLocalDriveService creates a new LocalDriveService
func NewLocalDriveService(remoteFolder string) *LocalDriveService {
return &LocalDriveService{
remoteFolder: remoteFolder,
}
}
// Login is a no-op for local filesystem
func (s *LocalDriveService) Login(ctx context.Context) error {
// Ensure the folder exists
return os.MkdirAll(s.remoteFolder, 0755)
}
// Upload creates a file with the given name and content
func (s *LocalDriveService) Upload(ctx context.Context, name string, data []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
filePath := filepath.Join(s.remoteFolder, name)
return os.WriteFile(filePath, data, 0644)
}
// List returns files matching the prefix, created within the given duration
func (s *LocalDriveService) List(ctx context.Context, prefix string, within time.Duration) ([]FileInfo, error) {
s.mu.RLock()
defer s.mu.RUnlock()
entries, err := os.ReadDir(s.remoteFolder)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
cutoff := time.Now().Add(-within)
var files []FileInfo
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !strings.HasPrefix(name, prefix) {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
if info.ModTime().Before(cutoff) {
continue
}
files = append(files, FileInfo{
Name: name,
CreatedAt: info.ModTime(),
})
}
return files, nil
}
// Download retrieves the content of a file
func (s *LocalDriveService) Download(ctx context.Context, name string) ([]byte, error) {
s.mu.RLock()
defer s.mu.RUnlock()
filePath := filepath.Join(s.remoteFolder, name)
return os.ReadFile(filePath)
}
// Delete removes a file
func (s *LocalDriveService) Delete(ctx context.Context, name string) error {
s.mu.Lock()
defer s.mu.Unlock()
filePath := filepath.Join(s.remoteFolder, name)
err := os.Remove(filePath)
if os.IsNotExist(err) {
return nil
}
return err
}
// ParseFileName parses a filename in format: sessionID-direction-seq
// Returns sessionID, direction ("up" or "down"), sequence number
func ParseFileName(name string) (sessionID string, direction string, seq int64, ok bool) {
// Remove extension if any
name = strings.TrimSuffix(name, filepath.Ext(name))
parts := strings.Split(name, "-")
if len(parts) < 7 { // UUID has 5 parts + direction + seq = at least 7
return "", "", 0, false
}
// UUID is the first 5 parts joined by "-"
sessionID = strings.Join(parts[:5], "-")
// Validate it's a UUID
if _, err := uuid.ParseString(sessionID); err != nil {
return "", "", 0, false
}
direction = parts[5]
if direction != "up" && direction != "down" {
return "", "", 0, false
}
seq, err := strconv.ParseInt(parts[6], 10, 64)
if err != nil {
return "", "", 0, false
}
return sessionID, direction, seq, true
}
// MakeFileName creates a filename in format: sessionID-direction-seq
func MakeFileName(sessionID, direction string, seq int64) string {
return fmt.Sprintf("%s-%s-%d", sessionID, direction, seq)
}
// XdriveConnection represents a connection over the XDRIVE transport
type XdriveConnection struct {
ctx context.Context
cancel context.CancelFunc
service DriveService
sessionID string
isClient bool // true for client, false for server
readDone *done.Instance
writeDone *done.Instance
readBuf []byte
readMu sync.Mutex
writeMu sync.Mutex
readSeq int64
writeSeq int64
localAddr net.Addr
remoteAddr net.Addr
}
// newXdriveConnection creates a new XdriveConnection
func newXdriveConnection(ctx context.Context, service DriveService, sessionID string, isClient bool) *XdriveConnection {
ctx, cancel := context.WithCancel(ctx)
return &XdriveConnection{
ctx: ctx,
cancel: cancel,
service: service,
sessionID: sessionID,
isClient: isClient,
readDone: done.New(),
writeDone: done.New(),
}
}
// Read reads data from the connection by polling for files
func (c *XdriveConnection) Read(b []byte) (int, error) {
c.readMu.Lock()
defer c.readMu.Unlock()
// If we have buffered data, return it first
if len(c.readBuf) > 0 {
n := copy(b, c.readBuf)
c.readBuf = c.readBuf[n:]
return n, nil
}
// Determine which direction to read from
// Client reads "down" (server to client), server reads "up" (client to server)
readDirection := "down"
if !c.isClient {
readDirection = "up"
}
prefix := fmt.Sprintf("%s-%s-", c.sessionID, readDirection)
// Poll for the expected file with timeout
deadline := time.Now().Add(ReadTimeout)
for time.Now().Before(deadline) {
select {
case <-c.ctx.Done():
return 0, io.EOF
case <-c.readDone.Wait():
return 0, io.EOF
default:
}
// List files within retention window
files, err := c.service.List(c.ctx, prefix, RetentionWindow)
if err != nil {
return 0, err
}
// Sort files by sequence number
sort.Slice(files, func(i, j int) bool {
_, _, seqI, _ := ParseFileName(files[i].Name)
_, _, seqJ, _ := ParseFileName(files[j].Name)
return seqI < seqJ
})
// Look for the expected sequence file
for _, file := range files {
_, _, seq, ok := ParseFileName(file.Name)
if !ok {
continue
}
if seq == c.readSeq {
// Found the expected file, download it
data, err := c.service.Download(c.ctx, file.Name)
if err != nil {
if os.IsNotExist(err) {
continue
}
return 0, err
}
// Delete the file after reading
c.service.Delete(c.ctx, file.Name)
c.readSeq++
// Copy to buffer
n := copy(b, data)
if n < len(data) {
c.readBuf = data[n:]
}
return n, nil
}
}
time.Sleep(ReadPollInterval)
}
return 0, errors.New("read timeout")
}
// Write writes data to the connection by creating files
func (c *XdriveConnection) Write(b []byte) (int, error) {
c.writeMu.Lock()
defer c.writeMu.Unlock()
select {
case <-c.ctx.Done():
return 0, io.ErrClosedPipe
case <-c.writeDone.Wait():
return 0, io.ErrClosedPipe
default:
}
// Determine which direction to write
// Client writes "up" (client to server), server writes "down" (server to client)
writeDirection := "up"
if !c.isClient {
writeDirection = "down"
}
fileName := MakeFileName(c.sessionID, writeDirection, c.writeSeq)
err := c.service.Upload(c.ctx, fileName, b)
if err != nil {
return 0, err
}
c.writeSeq++
return len(b), nil
}
// Close closes the connection
func (c *XdriveConnection) Close() error {
c.readDone.Close()
c.writeDone.Close()
c.cancel()
return nil
}
// LocalAddr returns the local address
func (c *XdriveConnection) LocalAddr() net.Addr {
if c.localAddr != nil {
return c.localAddr
}
return &net.TCPAddr{IP: net.IP{0, 0, 0, 0}, Port: 0}
}
// RemoteAddr returns the remote address
func (c *XdriveConnection) RemoteAddr() net.Addr {
if c.remoteAddr != nil {
return c.remoteAddr
}
return &net.TCPAddr{IP: net.IP{0, 0, 0, 0}, Port: 0}
}
// SetDeadline sets the deadline for the connection
func (c *XdriveConnection) SetDeadline(t time.Time) error {
return nil
}
// SetReadDeadline sets the read deadline for the connection
func (c *XdriveConnection) SetReadDeadline(t time.Time) error {
return nil
}
// SetWriteDeadline sets the write deadline for the connection
func (c *XdriveConnection) SetWriteDeadline(t time.Time) error {
return nil
}
func init() {
common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} {
return new(Config)
}))
common.Must(internet.RegisterTransportDialer(protocolName, Dial))
common.Must(internet.RegisterTransportListener(protocolName, Serve))
}
// createDriveService creates a DriveService based on the configuration
func createDriveService(config *Config) (DriveService, error) {
switch config.Service {
case "local":
return NewLocalDriveService(config.RemoteFolder), nil
case "Google Drive":
// Placeholder for Google Drive implementation
return nil, errors.New("Google Drive service not yet implemented")
default:
return nil, errors.New("unsupported service: " + config.Service)
}
}
// Dial creates a client connection to the XDRIVE transport
func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
config := streamSettings.ProtocolSettings.(*Config)
// Validate secrets - for client, secrets[0] should be "client"
if len(config.Secrets) == 0 || config.Secrets[0] != "client" {
return nil, errors.New("client must have secrets[0] set to 'client'")
}
service, err := createDriveService(config)
if err != nil {
return nil, err
}
if err := service.Login(ctx); err != nil {
return nil, errors.New("failed to login to drive service").Base(err)
}
// Generate a new session ID for this connection
newUUID := uuid.New()
sessionID := newUUID.String()
errors.LogInfo(ctx, fmt.Sprintf("XDRIVE client dialing with session %s to folder %s", sessionID, config.RemoteFolder))
conn := newXdriveConnection(ctx, service, sessionID, true)
return stat.Connection(conn), nil
}
// Server represents an XDRIVE server listener
type Server struct {
ctx context.Context
cancel context.CancelFunc
config *Config
service DriveService
addConn internet.ConnHandler
sessions sync.Map // sessionID -> *XdriveConnection
closeDone *done.Instance
}
// Close closes the server
func (s *Server) Close() error {
s.closeDone.Close()
s.cancel()
return nil
}
// Addr returns the address the server is listening on
func (s *Server) Addr() net.Addr {
return &net.TCPAddr{IP: net.IP{0, 0, 0, 0}, Port: 0}
}
// Serve creates a server listener for the XDRIVE transport
func Serve(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) {
config := streamSettings.ProtocolSettings.(*Config)
// Validate secrets - for server, secrets[0] should be "server"
if len(config.Secrets) == 0 || config.Secrets[0] != "server" {
return nil, errors.New("server must have secrets[0] set to 'server'")
}
service, err := createDriveService(config)
if err != nil {
return nil, err
}
if err := service.Login(ctx); err != nil {
return nil, errors.New("failed to login to drive service").Base(err)
}
ctx, cancel := context.WithCancel(ctx)
server := &Server{
ctx: ctx,
cancel: cancel,
config: config,
service: service,
addConn: addConn,
closeDone: done.New(),
}
errors.LogInfo(ctx, fmt.Sprintf("XDRIVE server listening on folder %s", config.RemoteFolder))
// Start polling for new connections
go server.pollForConnections()
// Start cleanup routine for old files
go server.cleanupOldFiles()
return server, nil
}
// pollForConnections polls the folder for new upload files indicating new connections
func (s *Server) pollForConnections() {
ticker := time.NewTicker(ServerPollInterval)
defer ticker.Stop()
for {
select {
case <-s.ctx.Done():
return
case <-s.closeDone.Wait():
return
case <-ticker.C:
s.checkForNewConnections()
}
}
}
// checkForNewConnections checks for files from new client sessions
func (s *Server) checkForNewConnections() {
// List all files within the retention window
files, err := s.service.List(s.ctx, "", RetentionWindow)
if err != nil {
errors.LogWarning(s.ctx, "failed to list files: ", err)
return
}
// Track seen session IDs
seenSessions := make(map[string]bool)
for _, file := range files {
sessionID, direction, _, ok := ParseFileName(file.Name)
if !ok {
continue
}
// Only process "up" files (client to server)
if direction != "up" {
continue
}
// Check if we already have a session for this ID
if seenSessions[sessionID] {
continue
}
seenSessions[sessionID] = true
if _, loaded := s.sessions.Load(sessionID); loaded {
continue
}
// New session detected, create a connection
conn := newXdriveConnection(s.ctx, s.service, sessionID, false)
s.sessions.Store(sessionID, conn)
errors.LogInfo(s.ctx, fmt.Sprintf("XDRIVE server accepted new connection: %s", sessionID))
// Handle the connection
s.addConn(stat.Connection(conn))
}
}
// cleanupOldFiles removes files older than the retention window
func (s *Server) cleanupOldFiles() {
ticker := time.NewTicker(CleanupInterval)
defer ticker.Stop()
for {
select {
case <-s.ctx.Done():
return
case <-s.closeDone.Wait():
return
case <-ticker.C:
s.doCleanup(RetentionWindow)
}
}
}
// doCleanup removes files older than the retention window
func (s *Server) doCleanup(retentionWindow time.Duration) {
localService, ok := s.service.(*LocalDriveService)
if !ok {
return
}
localService.mu.Lock()
defer localService.mu.Unlock()
entries, err := os.ReadDir(localService.remoteFolder)
if err != nil {
return
}
cutoff := time.Now().Add(-retentionWindow)
for _, entry := range entries {
if entry.IsDir() {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
if info.ModTime().Before(cutoff) {
filePath := filepath.Join(localService.remoteFolder, entry.Name())
os.Remove(filePath)
}
}
}
+109
View File
@@ -0,0 +1,109 @@
package xdrive_test
import (
"context"
"os"
"path/filepath"
"testing"
"time"
. "github.com/xtls/xray-core/transport/internet/xdrive"
)
func TestLocalDriveService(t *testing.T) {
// Create a temporary directory for testing
tmpDir := filepath.Join(os.TempDir(), "xdrive_test")
defer os.RemoveAll(tmpDir)
service := NewLocalDriveService(tmpDir)
ctx := context.Background()
// Test Login (creates directory)
if err := service.Login(ctx); err != nil {
t.Fatalf("Login failed: %v", err)
}
// Test Upload
testData := []byte("hello world")
testFileName := "test-file-1"
if err := service.Upload(ctx, testFileName, testData); err != nil {
t.Fatalf("Upload failed: %v", err)
}
// Test List
files, err := service.List(ctx, "test-", 10*time.Second)
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(files) != 1 {
t.Fatalf("Expected 1 file, got %d", len(files))
}
if files[0].Name != testFileName {
t.Fatalf("Expected file name %s, got %s", testFileName, files[0].Name)
}
// Test Download
downloaded, err := service.Download(ctx, testFileName)
if err != nil {
t.Fatalf("Download failed: %v", err)
}
if string(downloaded) != string(testData) {
t.Fatalf("Downloaded data mismatch: expected %s, got %s", string(testData), string(downloaded))
}
// Test Delete
if err := service.Delete(ctx, testFileName); err != nil {
t.Fatalf("Delete failed: %v", err)
}
// Verify file is deleted
files, err = service.List(ctx, "test-", 10*time.Second)
if err != nil {
t.Fatalf("List after delete failed: %v", err)
}
if len(files) != 0 {
t.Fatalf("Expected 0 files after delete, got %d", len(files))
}
}
func TestFileNameParsing(t *testing.T) {
// Test valid filename
sessionID := "550e8400-e29b-41d4-a716-446655440000"
fileName := sessionID + "-up-5"
parsed, direction, seq, ok := ParseFileName(fileName)
if !ok {
t.Fatalf("Failed to parse valid filename")
}
if parsed != sessionID {
t.Fatalf("Session ID mismatch: expected %s, got %s", sessionID, parsed)
}
if direction != "up" {
t.Fatalf("Direction mismatch: expected up, got %s", direction)
}
if seq != 5 {
t.Fatalf("Seq mismatch: expected 5, got %d", seq)
}
// Test invalid filenames
invalidNames := []string{
"invalid-file",
"550e8400-e29b-41d4-a716-446655440000-invalid-5",
"not-a-uuid-up-5",
}
for _, name := range invalidNames {
_, _, _, ok := ParseFileName(name)
if ok {
t.Fatalf("Expected parsing to fail for %s", name)
}
}
}
func TestMakeFileName(t *testing.T) {
sessionID := "550e8400-e29b-41d4-a716-446655440000"
expected := "550e8400-e29b-41d4-a716-446655440000-down-10"
result := MakeFileName(sessionID, "down", 10)
if result != expected {
t.Fatalf("MakeFileName mismatch: expected %s, got %s", expected, result)
}
}