mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-15 22:10:26 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ca6f4b7d4 | ||
|
|
18e283909c | ||
|
|
6ab123bf8f | ||
|
|
4aba687dd3 | ||
|
|
5b1b41058e | ||
|
|
6e3322d219 | ||
|
|
1d8eb81d70 | ||
|
|
e78d8ef184 | ||
|
|
6ce924ad56 | ||
|
|
035d438979 | ||
|
|
50231eaff9 | ||
|
|
1f74c480d6 | ||
|
|
af7eb68028 | ||
|
|
35387572e0 | ||
|
|
8f15190c23 | ||
|
|
64fada32b5 | ||
|
|
0bafca9486 | ||
|
|
d5bc58dc6b | ||
|
|
c18b39ed80 | ||
|
|
e2ad0acf60 | ||
|
|
c320e89108 | ||
|
|
412898fed7 | ||
|
|
5c62d50d43 | ||
|
|
1aabe7ea78 | ||
|
|
e4e7614c62 | ||
|
|
987290ba48 | ||
|
|
d7fa2076c3 | ||
|
|
fb548f54d2 | ||
|
|
0495b17650 | ||
|
|
65f6f0a43b | ||
|
|
3263ae9255 | ||
|
|
3dc8bf3d8b | ||
|
|
695e68ef9e | ||
|
|
dfdbcf86cc | ||
|
|
2b828b7bc2 |
@@ -37,6 +37,7 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
|
||||
|
||||
# Create log files
|
||||
|
||||
@@ -37,6 +37,7 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/08_fakedns.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/09_metrics.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/10_observatory.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
|
||||
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
|
||||
|
||||
# Create log files
|
||||
|
||||
@@ -64,6 +64,14 @@ jobs:
|
||||
echo "Latest: '$LATEST'."
|
||||
echo "LATEST=$LATEST" >>${GITHUB_ENV}
|
||||
|
||||
NEWEST=false
|
||||
if [[ "${{ github.event_name }}" == "release" ]]; then
|
||||
NEWEST=true
|
||||
fi
|
||||
|
||||
echo "Newest: '$NEWEST'."
|
||||
echo "NEWEST=$NEWEST" >>${GITHUB_ENV}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
@@ -124,6 +132,13 @@ jobs:
|
||||
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
|
||||
fi
|
||||
|
||||
if [[ "${{ env.NEWEST }}" == "true" ]]; then
|
||||
echo "Adding 'pre-release' tag to manifest: '${{ env.FULL_IMAGE_NAME }}:pre-release'."
|
||||
docker buildx imagetools create \
|
||||
--tag ${{ env.FULL_IMAGE_NAME }}:pre-release \
|
||||
${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
|
||||
fi
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ env.IMAGE_TAG }}
|
||||
@@ -131,3 +146,7 @@ jobs:
|
||||
if [[ "${{ env.LATEST }}" == "true" ]]; then
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:latest
|
||||
fi
|
||||
|
||||
if [[ "${{ env.NEWEST }}" == "true" ]]; then
|
||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:pre-release
|
||||
fi
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -193,7 +193,7 @@ jobs:
|
||||
echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v7
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v7
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
@@ -162,7 +162,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
|
||||
p := d.policy.ForLevel(user.Level)
|
||||
if p.Stats.UserUplink {
|
||||
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
|
||||
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
|
||||
if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
|
||||
inboundLink.Writer = &SizeStatWriter{
|
||||
Counter: c,
|
||||
Writer: inboundLink.Writer,
|
||||
@@ -171,7 +171,7 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
|
||||
}
|
||||
if p.Stats.UserDownlink {
|
||||
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
|
||||
if c, _ := stats.GetOrRegisterCounter(d.stats, name); c != nil {
|
||||
if c, _ := d.stats.GetOrRegisterCounter(name); c != nil {
|
||||
outboundLink.Writer = &SizeStatWriter{
|
||||
Counter: c,
|
||||
Writer: outboundLink.Writer,
|
||||
@@ -200,13 +200,13 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
|
||||
p := policyManager.ForLevel(user.Level)
|
||||
if p.Stats.UserUplink {
|
||||
name := "user>>>" + user.Email + ">>>traffic>>>uplink"
|
||||
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil {
|
||||
if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
|
||||
link.Reader.(*buf.TimeoutWrapperReader).Counter = c
|
||||
}
|
||||
}
|
||||
if p.Stats.UserDownlink {
|
||||
name := "user>>>" + user.Email + ">>>traffic>>>downlink"
|
||||
if c, _ := stats.GetOrRegisterCounter(statsManager, name); c != nil {
|
||||
if c, _ := statsManager.GetOrRegisterCounter(name); c != nil {
|
||||
link.Writer = &SizeStatWriter{
|
||||
Counter: c,
|
||||
Writer: link.Writer,
|
||||
@@ -223,7 +223,7 @@ func WrapLink(ctx context.Context, policyManager policy.Manager, statsManager st
|
||||
|
||||
func trackOnlineIP(ctx context.Context, sm stats.Manager, email, ip string) {
|
||||
name := "user>>>" + email + ">>>online"
|
||||
if om, _ := stats.GetOrRegisterOnlineMap(sm, name); om != nil {
|
||||
if om, _ := sm.GetOrRegisterOnlineMap(name); om != nil {
|
||||
om.AddIP(ip)
|
||||
context.AfterFunc(ctx, func() { om.RemoveIP(ip) })
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
|
||||
if len(tag) > 0 && policy.ForSystem().Stats.InboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
|
||||
if len(tag) > 0 && policy.ForSystem().Stats.InboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
|
||||
if len(tag) > 0 && policy.ForSystem().Stats.OutboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func getStatCounter(v *core.Instance, tag string) (stats.Counter, stats.Counter)
|
||||
if len(tag) > 0 && policy.ForSystem().Stats.OutboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
|
||||
@@ -48,6 +48,20 @@ func (m *Manager) RegisterCounter(name string) (stats.Counter, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetOrRegisterCounter implements stats.Manager.
|
||||
func (m *Manager) GetOrRegisterCounter(name string) (stats.Counter, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
if c, found := m.counters[name]; found {
|
||||
return c, nil
|
||||
}
|
||||
errors.LogDebug(context.Background(), "create new counter ", name)
|
||||
c := new(Counter)
|
||||
m.counters[name] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// UnregisterCounter implements stats.Manager.
|
||||
func (m *Manager) UnregisterCounter(name string) error {
|
||||
m.access.Lock()
|
||||
@@ -97,6 +111,20 @@ func (m *Manager) RegisterOnlineMap(name string) (stats.OnlineMap, error) {
|
||||
return om, nil
|
||||
}
|
||||
|
||||
// GetOrRegisterOnlineMap implements stats.Manager.
|
||||
func (m *Manager) GetOrRegisterOnlineMap(name string) (stats.OnlineMap, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
if om, found := m.onlineMaps[name]; found {
|
||||
return om, nil
|
||||
}
|
||||
errors.LogDebug(context.Background(), "create new OnlineMap ", name)
|
||||
om := NewOnlineMap()
|
||||
m.onlineMaps[name] = om
|
||||
return om, nil
|
||||
}
|
||||
|
||||
// UnregisterOnlineMap implements stats.Manager.
|
||||
func (m *Manager) UnregisterOnlineMap(name string) error {
|
||||
m.access.Lock()
|
||||
@@ -149,6 +177,26 @@ func (m *Manager) RegisterChannel(name string) (stats.Channel, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetOrRegisterChannel implements stats.Manager.
|
||||
func (m *Manager) GetOrRegisterChannel(name string) (stats.Channel, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
|
||||
if c, found := m.channels[name]; found {
|
||||
return c, nil
|
||||
}
|
||||
errors.LogDebug(context.Background(), "create new channel ", name)
|
||||
c := NewChannel(&ChannelConfig{BufferSize: 64, Blocking: false})
|
||||
if m.running {
|
||||
// Start before publishing so no goroutine can observe an unstarted channel.
|
||||
if err := c.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
m.channels[name] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// UnregisterChannel implements stats.Manager.
|
||||
func (m *Manager) UnregisterChannel(name string) error {
|
||||
m.access.Lock()
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ func NewReader(reader io.Reader) Reader {
|
||||
}
|
||||
|
||||
_, isFile := reader.(*os.File)
|
||||
if !isFile && useReadv {
|
||||
if !isFile && useReadV() {
|
||||
if sc, ok := reader.(syscall.Conn); ok {
|
||||
rawConn, err := sc.SyscallConn()
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ package buf
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
@@ -143,13 +144,24 @@ func (r *ReadVReader) ReadMultiBuffer() (MultiBuffer, error) {
|
||||
return mb, nil
|
||||
}
|
||||
|
||||
var useReadv bool
|
||||
var useReadv atomic.Bool
|
||||
|
||||
func init() {
|
||||
func useReadV() bool {
|
||||
return useReadv.Load()
|
||||
}
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
value := platform.NewEnvFlag(platform.UseReadV).GetValue(func() string { return defaultFlagValue })
|
||||
enabled := false
|
||||
switch value {
|
||||
case defaultFlagValue, "auto", "enable":
|
||||
useReadv = true
|
||||
enabled = true
|
||||
}
|
||||
useReadv.Store(enabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
"github.com/xtls/xray-core/features/stats"
|
||||
)
|
||||
|
||||
const useReadv = false
|
||||
func useReadV() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func NewReadVReader(reader io.Reader, rawConn syscall.RawConn, counter stats.Counter) Reader {
|
||||
panic("not implemented")
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package geodata
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
)
|
||||
|
||||
var privateIPMatcher = sync.OnceValue(func() IPMatcher {
|
||||
return common.Must2(IPReg.BuildIPMatcher(common.Must2(ParseIPRules([]string{
|
||||
"0.0.0.0/8",
|
||||
"10.0.0.0/8",
|
||||
"100.64.0.0/10",
|
||||
"127.0.0.0/8",
|
||||
"169.254.0.0/16",
|
||||
"172.16.0.0/12",
|
||||
"192.0.0.0/24",
|
||||
"192.0.2.0/24",
|
||||
"192.88.99.0/24",
|
||||
"192.168.0.0/16",
|
||||
"198.18.0.0/15",
|
||||
"198.51.100.0/24",
|
||||
"203.0.113.0/24",
|
||||
"224.0.0.0/3",
|
||||
"::/127",
|
||||
"fc00::/7",
|
||||
"fe80::/10",
|
||||
"ff00::/8",
|
||||
}))))
|
||||
})
|
||||
|
||||
func GetPrivateIPMatcher() IPMatcher { return privateIPMatcher() }
|
||||
|
||||
var privateDomainMatcher = sync.OnceValue(func() DomainMatcher {
|
||||
return common.Must2(DomainReg.BuildDomainMatcher(common.Must2(ParseDomainRules([]string{
|
||||
"lan",
|
||||
"localdomain",
|
||||
"example",
|
||||
"invalid",
|
||||
"localhost",
|
||||
"test",
|
||||
"local",
|
||||
"home.arpa",
|
||||
"internal",
|
||||
"regexp:^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$", // Dotless domains
|
||||
}, Domain_Domain))))
|
||||
})
|
||||
|
||||
func GetPrivateDomainMatcher() DomainMatcher { return privateDomainMatcher() }
|
||||
@@ -0,0 +1,348 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
darwinProcPIDListFDs = 1
|
||||
darwinProcPIDFDSocketInfo = 3
|
||||
darwinProcFDTypeSocket = 2
|
||||
darwinProcFDInfoSize = 8
|
||||
darwinSocketFDInfoSize = 792
|
||||
darwinSocketFDInfoPSIOff = 24
|
||||
darwinSocketInfoProtoOff = darwinSocketFDInfoPSIOff + 156
|
||||
darwinSocketInfoFamilyOff = darwinSocketFDInfoPSIOff + 160
|
||||
darwinSocketInfoKindOff = darwinSocketFDInfoPSIOff + 232
|
||||
darwinSocketInfoInSockOff = darwinSocketFDInfoPSIOff + 240
|
||||
darwinInSockInfoFPortOff = darwinSocketInfoInSockOff
|
||||
darwinInSockInfoLPortOff = darwinSocketInfoInSockOff + 4
|
||||
darwinInSockInfoVFlagOff = darwinSocketInfoInSockOff + 24
|
||||
darwinInSockInfoFAddrOff = darwinSocketInfoInSockOff + 32
|
||||
darwinInSockInfoLAddrOff = darwinSocketInfoInSockOff + 48
|
||||
darwinInSockInfoSize = 80
|
||||
darwinInSockInfoIPv4 = 0x1
|
||||
darwinInSockInfoIPv6 = 0x2
|
||||
darwinSockInfoIN = 1
|
||||
darwinSockInfoTCP = 2
|
||||
)
|
||||
|
||||
type darwinSocketMatchLevel int
|
||||
|
||||
const (
|
||||
darwinSocketNoMatch darwinSocketMatchLevel = iota
|
||||
darwinSocketPortMatch
|
||||
darwinSocketRemoteMatch
|
||||
darwinSocketLocalMatch
|
||||
darwinSocketExactMatch
|
||||
)
|
||||
|
||||
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (PID int, Name string, AbsolutePath string, err error) {
|
||||
isLocal, err := IsLocal(net.ParseIP(srcIP))
|
||||
if err != nil {
|
||||
return 0, "", "", errors.New("failed to determine if address is local: ", err)
|
||||
}
|
||||
if !isLocal {
|
||||
return 0, "", "", ErrNotLocal
|
||||
}
|
||||
if network != "tcp" && network != "udp" {
|
||||
panic("Unsupported network type for process lookup.")
|
||||
}
|
||||
|
||||
srcAddr, err := netip.ParseAddr(srcIP)
|
||||
if err != nil {
|
||||
return 0, "", "", errors.New("invalid source IP address: ", srcIP)
|
||||
}
|
||||
srcAddr = srcAddr.Unmap()
|
||||
|
||||
var dstAddr netip.Addr
|
||||
hasDstAddr := false
|
||||
if destIP != "" && destPort != 0 {
|
||||
dstAddr, err = netip.ParseAddr(destIP)
|
||||
if err != nil {
|
||||
return 0, "", "", errors.New("invalid destination IP address: ", destIP)
|
||||
}
|
||||
dstAddr = dstAddr.Unmap()
|
||||
hasDstAddr = true
|
||||
}
|
||||
|
||||
processes, err := unix.SysctlKinfoProcSlice("kern.proc.all")
|
||||
if err != nil {
|
||||
return 0, "", "", errors.New("failed to list processes").Base(err)
|
||||
}
|
||||
|
||||
var bestPID int32
|
||||
bestLevel := darwinSocketNoMatch
|
||||
ambiguousBest := false
|
||||
|
||||
for _, process := range processes {
|
||||
pid := process.Proc.P_pid
|
||||
if pid <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
matchLevel, err := darwinProcessSocketMatchLevel(pid, network, srcAddr, srcPort, dstAddr, destPort, hasDstAddr)
|
||||
if err != nil || matchLevel == darwinSocketNoMatch {
|
||||
continue
|
||||
}
|
||||
if matchLevel == darwinSocketExactMatch {
|
||||
bestPID = pid
|
||||
bestLevel = matchLevel
|
||||
ambiguousBest = false
|
||||
break
|
||||
}
|
||||
if matchLevel > bestLevel {
|
||||
bestPID = pid
|
||||
bestLevel = matchLevel
|
||||
ambiguousBest = false
|
||||
continue
|
||||
}
|
||||
if matchLevel == bestLevel {
|
||||
ambiguousBest = true
|
||||
}
|
||||
}
|
||||
|
||||
if bestLevel == darwinSocketNoMatch {
|
||||
return 0, "", "", errors.New("process not found for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
|
||||
}
|
||||
if ambiguousBest {
|
||||
return 0, "", "", errors.New("ambiguous process match for ", network, " connection from ", srcIP, ":", srcPort, " to ", destIP, ":", destPort)
|
||||
}
|
||||
|
||||
absPath, err := darwinProcessPath(bestPID)
|
||||
if err != nil {
|
||||
return 0, "", "", errors.New("could not get process path for PID ", bestPID, ": ", err)
|
||||
}
|
||||
|
||||
absPath = filepath.ToSlash(absPath)
|
||||
return int(bestPID), filepath.Base(absPath), absPath, nil
|
||||
}
|
||||
|
||||
func darwinProcessSocketMatchLevel(pid int32, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) (darwinSocketMatchLevel, error) {
|
||||
fds, err := darwinProcessFDs(pid)
|
||||
if err != nil {
|
||||
return darwinSocketNoMatch, err
|
||||
}
|
||||
|
||||
bestLevel := darwinSocketNoMatch
|
||||
info := make([]byte, darwinSocketFDInfoSize)
|
||||
for fd := 0; fd+darwinProcFDInfoSize <= len(fds); fd += darwinProcFDInfoSize {
|
||||
fdNumber := int32(darwinReadNativeUint32(fds[fd : fd+4]))
|
||||
fdType := darwinReadNativeUint32(fds[fd+4 : fd+8])
|
||||
if fdType != darwinProcFDTypeSocket {
|
||||
continue
|
||||
}
|
||||
|
||||
n, err := darwinProcPIDFDInfo(pid, fdNumber, darwinProcPIDFDSocketInfo, info)
|
||||
if err != nil || n < darwinSocketInfoInSockOff+darwinInSockInfoSize {
|
||||
continue
|
||||
}
|
||||
level := darwinSocketInfoMatchLevel(info[:n], network, srcAddr, srcPort, dstAddr, dstPort, hasDstAddr)
|
||||
if level == darwinSocketExactMatch {
|
||||
return level, nil
|
||||
}
|
||||
if level > bestLevel {
|
||||
bestLevel = level
|
||||
}
|
||||
}
|
||||
|
||||
return bestLevel, nil
|
||||
}
|
||||
|
||||
func darwinProcessFDs(pid int32) ([]byte, error) {
|
||||
n, err := darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
buf := make([]byte, n)
|
||||
n, err = darwinProcPIDInfo(pid, darwinProcPIDListFDs, 0, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf[:n], nil
|
||||
}
|
||||
|
||||
func darwinSocketInfoMatchLevel(info []byte, network string, srcAddr netip.Addr, srcPort uint16, dstAddr netip.Addr, dstPort uint16, hasDstAddr bool) darwinSocketMatchLevel {
|
||||
protocol := int(darwinReadNativeUint32(info[darwinSocketInfoProtoOff : darwinSocketInfoProtoOff+4]))
|
||||
family := int(darwinReadNativeUint32(info[darwinSocketInfoFamilyOff : darwinSocketInfoFamilyOff+4]))
|
||||
kind := int(darwinReadNativeUint32(info[darwinSocketInfoKindOff : darwinSocketInfoKindOff+4]))
|
||||
|
||||
switch network {
|
||||
case "tcp":
|
||||
if protocol != unix.IPPROTO_TCP || kind != darwinSockInfoTCP {
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
case "udp":
|
||||
if protocol != unix.IPPROTO_UDP || kind != darwinSockInfoIN {
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
default:
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
|
||||
vflag := info[darwinInSockInfoVFlagOff]
|
||||
if srcAddr.Is4() {
|
||||
if family != unix.AF_INET || vflag&darwinInSockInfoIPv4 == 0 {
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
} else {
|
||||
if family != unix.AF_INET6 || vflag&darwinInSockInfoIPv6 == 0 {
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
}
|
||||
|
||||
localPort := int32(darwinReadNativeUint32(info[darwinInSockInfoLPortOff : darwinInSockInfoLPortOff+4]))
|
||||
if !darwinPortMatches(localPort, srcPort) {
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
localAddrMatches := darwinAddrMatchesOrUnspecified(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], srcAddr)
|
||||
|
||||
foreignAddrRaw := info[darwinInSockInfoFAddrOff : darwinInSockInfoFAddrOff+16]
|
||||
foreignPort := int32(darwinReadNativeUint32(info[darwinInSockInfoFPortOff : darwinInSockInfoFPortOff+4]))
|
||||
|
||||
if !hasDstAddr {
|
||||
if localAddrMatches {
|
||||
return darwinSocketExactMatch
|
||||
}
|
||||
return darwinSocketNoMatch
|
||||
}
|
||||
|
||||
remoteMatches := darwinPortMatches(foreignPort, dstPort) && darwinAddrMatches(foreignAddrRaw, dstAddr)
|
||||
if network == "udp" && darwinEndpointIsZero(foreignAddrRaw, foreignPort) && localAddrMatches {
|
||||
return darwinSocketExactMatch
|
||||
}
|
||||
switch {
|
||||
case localAddrMatches && remoteMatches:
|
||||
return darwinSocketExactMatch
|
||||
case localAddrMatches:
|
||||
return darwinSocketLocalMatch
|
||||
case remoteMatches:
|
||||
return darwinSocketRemoteMatch
|
||||
default:
|
||||
return darwinSocketPortMatch
|
||||
}
|
||||
}
|
||||
|
||||
func darwinPortMatches(value int32, port uint16) bool {
|
||||
raw := uint16(value)
|
||||
return raw == port || darwinNtohs(raw) == port
|
||||
}
|
||||
|
||||
func darwinNtohs(value uint16) uint16 {
|
||||
return value<<8 | value>>8
|
||||
}
|
||||
|
||||
func darwinAddrMatches(raw []byte, addr netip.Addr) bool {
|
||||
if addr.Is4() {
|
||||
ip := addr.As4()
|
||||
return bytes.Equal(raw[12:16], ip[:])
|
||||
}
|
||||
ip := addr.As16()
|
||||
return bytes.Equal(raw, ip[:])
|
||||
}
|
||||
|
||||
func darwinAddrMatchesOrUnspecified(raw []byte, addr netip.Addr) bool {
|
||||
if darwinAddrMatches(raw, addr) {
|
||||
return true
|
||||
}
|
||||
if addr.Is4() {
|
||||
return darwinBytesAreZero(raw[12:16])
|
||||
}
|
||||
return darwinBytesAreZero(raw)
|
||||
}
|
||||
|
||||
func darwinEndpointIsZero(rawAddr []byte, port int32) bool {
|
||||
return uint32(port) == 0 && darwinBytesAreZero(rawAddr)
|
||||
}
|
||||
|
||||
func darwinBytesAreZero(raw []byte) bool {
|
||||
for _, value := range raw {
|
||||
if value != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func darwinReadNativeUint32(b []byte) uint32 {
|
||||
return *(*uint32)(unsafe.Pointer(&b[0]))
|
||||
}
|
||||
|
||||
func darwinProcessPath(pid int32) (string, error) {
|
||||
buf := make([]byte, unix.PathMax)
|
||||
n, err := darwinProcPIDPath(pid, buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n <= 0 {
|
||||
return "", errors.New("empty process path")
|
||||
}
|
||||
return strings.TrimRight(string(buf[:n]), "\x00"), nil
|
||||
}
|
||||
|
||||
func darwinProcPIDInfo(pid int32, flavor int, arg uint64, buf []byte) (int, error) {
|
||||
var ptr unsafe.Pointer
|
||||
if len(buf) > 0 {
|
||||
ptr = unsafe.Pointer(&buf[0])
|
||||
}
|
||||
|
||||
r0, _, errno := syscall_syscall6(libc_proc_pidinfo_trampoline_addr, uintptr(pid), uintptr(flavor), uintptr(arg), uintptr(ptr), uintptr(len(buf)), 0)
|
||||
if errno != 0 {
|
||||
return 0, errno
|
||||
}
|
||||
return int(r0), nil
|
||||
}
|
||||
|
||||
func darwinProcPIDFDInfo(pid int32, fd int32, flavor int, buf []byte) (int, error) {
|
||||
var ptr unsafe.Pointer
|
||||
if len(buf) > 0 {
|
||||
ptr = unsafe.Pointer(&buf[0])
|
||||
}
|
||||
|
||||
r0, _, errno := syscall_syscall6(libc_proc_pidfdinfo_trampoline_addr, uintptr(pid), uintptr(fd), uintptr(flavor), uintptr(ptr), uintptr(len(buf)), 0)
|
||||
if errno != 0 {
|
||||
return 0, errno
|
||||
}
|
||||
return int(r0), nil
|
||||
}
|
||||
|
||||
func darwinProcPIDPath(pid int32, buf []byte) (int, error) {
|
||||
r0, _, errno := syscall_syscall6(libc_proc_pidpath_trampoline_addr, uintptr(pid), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0, 0, 0)
|
||||
if errno != 0 {
|
||||
return 0, errno
|
||||
}
|
||||
return int(r0), nil
|
||||
}
|
||||
|
||||
var libc_proc_pidinfo_trampoline_addr uintptr
|
||||
|
||||
//go:cgo_import_dynamic libc_proc_pidinfo proc_pidinfo "/usr/lib/libproc.dylib"
|
||||
|
||||
var libc_proc_pidfdinfo_trampoline_addr uintptr
|
||||
|
||||
//go:cgo_import_dynamic libc_proc_pidfdinfo proc_pidfdinfo "/usr/lib/libproc.dylib"
|
||||
|
||||
var libc_proc_pidpath_trampoline_addr uintptr
|
||||
|
||||
//go:cgo_import_dynamic libc_proc_pidpath proc_pidpath "/usr/lib/libproc.dylib"
|
||||
|
||||
// Implemented in the runtime package (runtime/sys_darwin.go).
|
||||
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
|
||||
|
||||
//go:linkname syscall_syscall6 syscall.syscall6
|
||||
@@ -0,0 +1,18 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
TEXT libc_proc_pidinfo_trampoline<>(SB),NOSPLIT,$0-0
|
||||
JMP libc_proc_pidinfo(SB)
|
||||
GLOBL ·libc_proc_pidinfo_trampoline_addr(SB), RODATA, $8
|
||||
DATA ·libc_proc_pidinfo_trampoline_addr(SB)/8, $libc_proc_pidinfo_trampoline<>(SB)
|
||||
|
||||
TEXT libc_proc_pidfdinfo_trampoline<>(SB),NOSPLIT,$0-0
|
||||
JMP libc_proc_pidfdinfo(SB)
|
||||
GLOBL ·libc_proc_pidfdinfo_trampoline_addr(SB), RODATA, $8
|
||||
DATA ·libc_proc_pidfdinfo_trampoline_addr(SB)/8, $libc_proc_pidfdinfo_trampoline<>(SB)
|
||||
|
||||
TEXT libc_proc_pidpath_trampoline<>(SB),NOSPLIT,$0-0
|
||||
JMP libc_proc_pidpath(SB)
|
||||
GLOBL ·libc_proc_pidpath_trampoline_addr(SB), RODATA, $8
|
||||
DATA ·libc_proc_pidpath_trampoline_addr(SB)/8, $libc_proc_pidpath_trampoline<>(SB)
|
||||
@@ -0,0 +1,293 @@
|
||||
//go:build darwin && !ios
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
stdnet "net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestFindProcessDarwinTCP(t *testing.T) {
|
||||
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
accepted := make(chan stdnet.Conn, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err == nil {
|
||||
accepted <- conn
|
||||
return
|
||||
}
|
||||
close(accepted)
|
||||
}()
|
||||
|
||||
conn, err := stdnet.Dial("tcp", listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
serverConn := <-accepted
|
||||
if serverConn == nil {
|
||||
t.Fatal("server did not accept tcp connection")
|
||||
}
|
||||
defer serverConn.Close()
|
||||
|
||||
local := conn.LocalAddr().(*stdnet.TCPAddr)
|
||||
remote := conn.RemoteAddr().(*stdnet.TCPAddr)
|
||||
|
||||
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), remote.IP.String(), uint16(remote.Port))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCurrentProcess(t, pid, name, path)
|
||||
}
|
||||
|
||||
func TestFindProcessDarwinUDP(t *testing.T) {
|
||||
conn, err := stdnet.ListenUDP("udp", &stdnet.UDPAddr{IP: stdnet.ParseIP("127.0.0.1")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
local := conn.LocalAddr().(*stdnet.UDPAddr)
|
||||
|
||||
pid, name, path, err := FindProcess("udp", local.IP.String(), uint16(local.Port), "", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCurrentProcess(t, pid, name, path)
|
||||
}
|
||||
|
||||
func TestFindProcessDarwinNonLocal(t *testing.T) {
|
||||
_, _, _, err := FindProcess("tcp", "203.0.113.1", 80, "", 0)
|
||||
if err != ErrNotLocal {
|
||||
t.Fatalf("expected ErrNotLocal, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindProcessDarwinUnsupportedNetwork(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("expected panic")
|
||||
}
|
||||
}()
|
||||
|
||||
_, _, _, _ = FindProcess("icmp", "127.0.0.1", 0, "", 0)
|
||||
}
|
||||
|
||||
func assertCurrentProcess(t *testing.T, pid int, name string, path string) {
|
||||
t.Helper()
|
||||
|
||||
if pid != os.Getpid() {
|
||||
t.Fatalf("expected pid %d, got %d (%s, %s)", os.Getpid(), pid, name, path)
|
||||
}
|
||||
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if path == "" || name == "" {
|
||||
t.Fatalf("expected process path and name, got name=%q path=%q", name, path)
|
||||
}
|
||||
if sameFile(executable, path) {
|
||||
return
|
||||
}
|
||||
t.Fatalf("expected executable %q, got %q", executable, path)
|
||||
}
|
||||
|
||||
func sameFile(left string, right string) bool {
|
||||
leftInfo, leftErr := os.Stat(left)
|
||||
rightInfo, rightErr := os.Stat(right)
|
||||
if leftErr != nil || rightErr != nil {
|
||||
return false
|
||||
}
|
||||
return os.SameFile(leftInfo, rightInfo)
|
||||
}
|
||||
|
||||
func TestFindProcessDarwinTCPWithoutDestination(t *testing.T) {
|
||||
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
accepted := make(chan stdnet.Conn, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err == nil {
|
||||
accepted <- conn
|
||||
return
|
||||
}
|
||||
close(accepted)
|
||||
}()
|
||||
|
||||
conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
serverConn := <-accepted
|
||||
if serverConn == nil {
|
||||
t.Fatal("server did not accept tcp connection")
|
||||
}
|
||||
defer serverConn.Close()
|
||||
|
||||
local := conn.LocalAddr().(*stdnet.TCPAddr)
|
||||
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCurrentProcess(t, pid, name, path)
|
||||
}
|
||||
|
||||
func TestFindProcessDarwinTCPWithDifferentDestination(t *testing.T) {
|
||||
listener, err := stdnet.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
accepted := make(chan stdnet.Conn, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err == nil {
|
||||
accepted <- conn
|
||||
return
|
||||
}
|
||||
close(accepted)
|
||||
}()
|
||||
|
||||
conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
serverConn := <-accepted
|
||||
if serverConn == nil {
|
||||
t.Fatal("server did not accept tcp connection")
|
||||
}
|
||||
defer serverConn.Close()
|
||||
|
||||
local := conn.LocalAddr().(*stdnet.TCPAddr)
|
||||
pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "203.0.113.10", 443)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertCurrentProcess(t, pid, name, path)
|
||||
}
|
||||
|
||||
func TestDarwinSocketInfoMatchLevelFallbacks(t *testing.T) {
|
||||
src := netip.MustParseAddr("198.18.0.2")
|
||||
dst := netip.MustParseAddr("203.0.113.10")
|
||||
otherLocal := netip.MustParseAddr("192.168.1.10")
|
||||
otherRemote := netip.MustParseAddr("198.51.100.10")
|
||||
unspecifiedLocal := netip.MustParseAddr("0.0.0.0")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
local netip.Addr
|
||||
remote netip.Addr
|
||||
hasDst bool
|
||||
wantLevel darwinSocketMatchLevel
|
||||
}{
|
||||
{
|
||||
name: "exact",
|
||||
local: src,
|
||||
remote: dst,
|
||||
hasDst: true,
|
||||
wantLevel: darwinSocketExactMatch,
|
||||
},
|
||||
{
|
||||
name: "unspecified local with matching remote",
|
||||
local: unspecifiedLocal,
|
||||
remote: dst,
|
||||
hasDst: true,
|
||||
wantLevel: darwinSocketExactMatch,
|
||||
},
|
||||
{
|
||||
name: "unspecified local without destination",
|
||||
local: unspecifiedLocal,
|
||||
remote: otherRemote,
|
||||
hasDst: false,
|
||||
wantLevel: darwinSocketExactMatch,
|
||||
},
|
||||
{
|
||||
name: "local match with different remote",
|
||||
local: src,
|
||||
remote: otherRemote,
|
||||
hasDst: true,
|
||||
wantLevel: darwinSocketLocalMatch,
|
||||
},
|
||||
{
|
||||
name: "remote match with different local",
|
||||
local: otherLocal,
|
||||
remote: dst,
|
||||
hasDst: true,
|
||||
wantLevel: darwinSocketRemoteMatch,
|
||||
},
|
||||
{
|
||||
name: "port only with destination",
|
||||
local: otherLocal,
|
||||
remote: otherRemote,
|
||||
hasDst: true,
|
||||
wantLevel: darwinSocketPortMatch,
|
||||
},
|
||||
{
|
||||
name: "different local without destination",
|
||||
local: otherLocal,
|
||||
remote: otherRemote,
|
||||
hasDst: false,
|
||||
wantLevel: darwinSocketNoMatch,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
info := newDarwinSocketInfo("tcp", test.local, 12345, test.remote, 443)
|
||||
level := darwinSocketInfoMatchLevel(info, "tcp", src, 12345, dst, 443, test.hasDst)
|
||||
if level != test.wantLevel {
|
||||
t.Fatalf("unexpected match level: got %d, want %d", level, test.wantLevel)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newDarwinSocketInfo(network string, local netip.Addr, localPort uint16, remote netip.Addr, remotePort uint16) []byte {
|
||||
info := make([]byte, darwinSocketFDInfoSize)
|
||||
switch network {
|
||||
case "tcp":
|
||||
writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_TCP))
|
||||
writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoTCP))
|
||||
case "udp":
|
||||
writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_UDP))
|
||||
writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoIN))
|
||||
}
|
||||
writeDarwinNativeUint32(info, darwinSocketInfoFamilyOff, uint32(unix.AF_INET))
|
||||
info[darwinInSockInfoVFlagOff] = darwinInSockInfoIPv4
|
||||
writeDarwinNativeUint32(info, darwinInSockInfoLPortOff, uint32(localPort))
|
||||
writeDarwinNativeUint32(info, darwinInSockInfoFPortOff, uint32(remotePort))
|
||||
copyDarwinIPv4(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], local)
|
||||
copyDarwinIPv4(info[darwinInSockInfoFAddrOff:darwinInSockInfoFAddrOff+16], remote)
|
||||
return info
|
||||
}
|
||||
|
||||
func writeDarwinNativeUint32(b []byte, offset int, value uint32) {
|
||||
*(*uint32)(unsafe.Pointer(&b[offset])) = value
|
||||
}
|
||||
|
||||
func copyDarwinIPv4(dst []byte, addr netip.Addr) {
|
||||
ip := addr.As4()
|
||||
copy(dst[12:16], ip[:])
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build ios
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort uint16) (int, string, string, error) {
|
||||
return 0, "", "", errors.New("process lookup is not supported on this platform")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !windows && !linux && !android
|
||||
//go:build !windows && !linux && !android && !darwin
|
||||
|
||||
package net
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var envReloadRegistry = struct {
|
||||
sync.RWMutex
|
||||
handlers []func() error
|
||||
}{}
|
||||
|
||||
// RegisterEnvReload registers an environment reload handler and runs it once
|
||||
// immediately so package defaults keep the same behavior as init-time reads.
|
||||
func RegisterEnvReload(handler func() error) {
|
||||
if handler == nil {
|
||||
return
|
||||
}
|
||||
envReloadRegistry.Lock()
|
||||
envReloadRegistry.handlers = append(envReloadRegistry.handlers, handler)
|
||||
envReloadRegistry.Unlock()
|
||||
if err := handler(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ReloadEnvSettings refreshes all registered environment-backed package state.
|
||||
func ReloadEnvSettings() error {
|
||||
envReloadRegistry.RLock()
|
||||
handlers := append([]func() error{}, envReloadRegistry.handlers...)
|
||||
envReloadRegistry.RUnlock()
|
||||
|
||||
var errs []error
|
||||
for _, handler := range handlers {
|
||||
if err := handler(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
@@ -28,8 +28,6 @@ const (
|
||||
SecurityType_AUTO SecurityType = 2
|
||||
SecurityType_AES128_GCM SecurityType = 3
|
||||
SecurityType_CHACHA20_POLY1305 SecurityType = 4
|
||||
SecurityType_NONE SecurityType = 5 // [DEPRECATED 2023-06]
|
||||
SecurityType_ZERO SecurityType = 6
|
||||
)
|
||||
|
||||
// Enum value maps for SecurityType.
|
||||
@@ -39,16 +37,12 @@ var (
|
||||
2: "AUTO",
|
||||
3: "AES128_GCM",
|
||||
4: "CHACHA20_POLY1305",
|
||||
5: "NONE",
|
||||
6: "ZERO",
|
||||
}
|
||||
SecurityType_value = map[string]int32{
|
||||
"UNKNOWN": 0,
|
||||
"AUTO": 2,
|
||||
"AES128_GCM": 3,
|
||||
"CHACHA20_POLY1305": 4,
|
||||
"NONE": 5,
|
||||
"ZERO": 6,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -129,15 +123,13 @@ const file_common_protocol_headers_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x1dcommon/protocol/headers.proto\x12\x14xray.common.protocol\"H\n" +
|
||||
"\x0eSecurityConfig\x126\n" +
|
||||
"\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*`\n" +
|
||||
"\x04type\x18\x01 \x01(\x0e2\".xray.common.protocol.SecurityTypeR\x04type*L\n" +
|
||||
"\fSecurityType\x12\v\n" +
|
||||
"\aUNKNOWN\x10\x00\x12\b\n" +
|
||||
"\x04AUTO\x10\x02\x12\x0e\n" +
|
||||
"\n" +
|
||||
"AES128_GCM\x10\x03\x12\x15\n" +
|
||||
"\x11CHACHA20_POLY1305\x10\x04\x12\b\n" +
|
||||
"\x04NONE\x10\x05\x12\b\n" +
|
||||
"\x04ZERO\x10\x06B^\n" +
|
||||
"\x11CHACHA20_POLY1305\x10\x04B^\n" +
|
||||
"\x18com.xray.common.protocolP\x01Z)github.com/xtls/xray-core/common/protocol\xaa\x02\x14Xray.Common.Protocolb\x06proto3"
|
||||
|
||||
var (
|
||||
|
||||
@@ -11,8 +11,6 @@ enum SecurityType {
|
||||
AUTO = 2;
|
||||
AES128_GCM = 3;
|
||||
CHACHA20_POLY1305 = 4;
|
||||
NONE = 5; // [DEPRECATED 2023-06]
|
||||
ZERO = 6;
|
||||
}
|
||||
|
||||
message SecurityConfig {
|
||||
|
||||
@@ -143,7 +143,9 @@ func SniffQUIC(b []byte) (*SniffHeader, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(b) < hdrLen+4+block.BlockSize() {
|
||||
return nil, errNotQuic
|
||||
}
|
||||
cache.Clear()
|
||||
mask := cache.Extend(int32(block.BlockSize()))
|
||||
block.Encrypt(mask, b[hdrLen+4:hdrLen+4+len(mask)])
|
||||
|
||||
+33
-19
@@ -8,7 +8,7 @@ import (
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -27,25 +27,39 @@ var AddrParser = protocol.NewAddressParser(
|
||||
)
|
||||
|
||||
var (
|
||||
Show bool
|
||||
BaseKey []byte
|
||||
Show atomic.Bool
|
||||
baseKey atomic.Value
|
||||
)
|
||||
|
||||
func init() {
|
||||
if strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true" {
|
||||
Show = true
|
||||
func reloadEnvSettings() error {
|
||||
Show.Store(strings.ToLower(platform.NewEnvFlag(platform.XUDPLog).GetValue(func() string { return "" })) == "true")
|
||||
raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" })
|
||||
if raw == "" {
|
||||
ensureBaseKey()
|
||||
return nil
|
||||
}
|
||||
BaseKey = make([]byte, 32)
|
||||
rand.Read(BaseKey)
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond) // this is not nice, but need to give some time for Android to setup ENV
|
||||
if raw := platform.NewEnvFlag(platform.XUDPBaseKey).GetValue(func() string { return "" }); raw != "" {
|
||||
if BaseKey, _ = base64.RawURLEncoding.DecodeString(raw); len(BaseKey) == 32 {
|
||||
return
|
||||
}
|
||||
panic(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(BaseKey)))
|
||||
}
|
||||
}()
|
||||
key, _ := base64.RawURLEncoding.DecodeString(raw)
|
||||
if len(key) != 32 {
|
||||
return errors.New(platform.XUDPBaseKey + ": invalid value (BaseKey must be 32 bytes): " + raw + " len " + strconv.Itoa(len(key)))
|
||||
}
|
||||
baseKey.Store(append([]byte(nil), key...))
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureBaseKey() []byte {
|
||||
if key := baseKey.Load(); key != nil {
|
||||
return key.([]byte)
|
||||
}
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
baseKey.Store(key)
|
||||
return key
|
||||
}
|
||||
|
||||
func init() {
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
}
|
||||
|
||||
func GetGlobalID(ctx context.Context) (globalID [8]byte) {
|
||||
@@ -54,10 +68,10 @@ func GetGlobalID(ctx context.Context) (globalID [8]byte) {
|
||||
}
|
||||
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Source.Network == net.Network_UDP &&
|
||||
(inbound.Name == "dokodemo-door" || inbound.Name == "socks" || inbound.Name == "shadowsocks" || inbound.Name == "tun") {
|
||||
h := blake3.New(8, BaseKey)
|
||||
h := blake3.New(8, ensureBaseKey())
|
||||
h.Write([]byte(inbound.Source.String()))
|
||||
copy(globalID[:], h.Sum(nil))
|
||||
if Show {
|
||||
if Show.Load() {
|
||||
errors.LogInfo(ctx, fmt.Sprintf("XUDP inbound.Source.String(): %v\tglobalID: %v\n", inbound.Source.String(), globalID))
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ import (
|
||||
|
||||
var (
|
||||
Version_x byte = 26
|
||||
Version_y byte = 6
|
||||
Version_z byte = 27
|
||||
Version_y byte = 7
|
||||
Version_z byte = 28
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -187,6 +187,9 @@ func NewWithContext(ctx context.Context, config *Config) (*Instance, error) {
|
||||
}
|
||||
|
||||
func initInstanceWithConfig(config *Config, server *Instance) (bool, error) {
|
||||
if err := platform.ReloadEnvSettings(); err != nil {
|
||||
return true, errors.New("failed to reload environment settings").Base(err)
|
||||
}
|
||||
server.ctx = context.WithValue(server.ctx, "cone",
|
||||
platform.NewEnvFlag(platform.UseCone).GetValue(func() string { return "" }) != "true")
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package policy
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
@@ -82,32 +83,41 @@ func ManagerType() interface{} {
|
||||
return (*Manager)(nil)
|
||||
}
|
||||
|
||||
var defaultBufferSize int32
|
||||
var defaultBufferSize atomic.Int32
|
||||
|
||||
func init() {
|
||||
func reloadEnvSettings() error {
|
||||
defaultBufferSize.Store(readDefaultBufferSize())
|
||||
return nil
|
||||
}
|
||||
|
||||
func readDefaultBufferSize() int32 {
|
||||
const defaultValue = -17
|
||||
size := platform.NewEnvFlag(platform.BufferSize).GetValueAsInt(defaultValue)
|
||||
|
||||
switch size {
|
||||
case 0:
|
||||
defaultBufferSize = -1 // For pipe to use unlimited size
|
||||
return -1 // For pipe to use unlimited size
|
||||
case defaultValue: // Env flag not defined. Use default values per CPU-arch.
|
||||
switch runtime.GOARCH {
|
||||
case "arm", "mips", "mipsle":
|
||||
defaultBufferSize = 0
|
||||
return 0
|
||||
case "arm64", "mips64", "mips64le":
|
||||
defaultBufferSize = 4 * 1024 // 4k cache for low-end devices
|
||||
return 4 * 1024 // 4k cache for low-end devices
|
||||
default:
|
||||
defaultBufferSize = 512 * 1024
|
||||
return 512 * 1024
|
||||
}
|
||||
default:
|
||||
defaultBufferSize = int32(size) * 1024 * 1024
|
||||
return int32(size) * 1024 * 1024
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
}
|
||||
|
||||
func defaultBufferPolicy() Buffer {
|
||||
return Buffer{
|
||||
PerConnection: defaultBufferSize,
|
||||
PerConnection: defaultBufferSize.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-30
@@ -81,6 +81,8 @@ type Manager interface {
|
||||
|
||||
// RegisterCounter registers a new counter to the manager. The identifier string must not be empty, and unique among other counters.
|
||||
RegisterCounter(string) (Counter, error)
|
||||
// GetOrRegisterCounter returns the counter by its identifier, atomically creating and registering it if absent.
|
||||
GetOrRegisterCounter(string) (Counter, error)
|
||||
// UnregisterCounter unregisters a counter from the manager by its identifier.
|
||||
UnregisterCounter(string) error
|
||||
// GetCounter returns a counter by its identifier.
|
||||
@@ -91,6 +93,8 @@ type Manager interface {
|
||||
|
||||
// RegisterOnlineMap registers a new OnlineMap to the manager. The identifier string must not be empty, and unique among other OnlineMaps.
|
||||
RegisterOnlineMap(string) (OnlineMap, error)
|
||||
// GetOrRegisterOnlineMap returns the OnlineMap by its identifier, atomically creating and registering it if absent.
|
||||
GetOrRegisterOnlineMap(string) (OnlineMap, error)
|
||||
// UnregisterOnlineMap unregisters an OnlineMap from the manager by its identifier.
|
||||
UnregisterOnlineMap(string) error
|
||||
// GetOnlineMap returns an OnlineMap by its identifier.
|
||||
@@ -101,6 +105,8 @@ type Manager interface {
|
||||
|
||||
// RegisterChannel registers a new channel to the manager. The identifier string must not be empty, and unique among other channels.
|
||||
RegisterChannel(string) (Channel, error)
|
||||
// GetOrRegisterChannel returns the channel by its identifier, atomically creating and registering it if absent.
|
||||
GetOrRegisterChannel(string) (Channel, error)
|
||||
// UnregisterChannel unregisters a channel from the manager by its identifier.
|
||||
UnregisterChannel(string) error
|
||||
// GetChannel returns a channel by its identifier.
|
||||
@@ -110,36 +116,6 @@ type Manager interface {
|
||||
GetAllOnlineUsers() []string
|
||||
}
|
||||
|
||||
// GetOrRegisterCounter tries to get the StatCounter first. If not exist, it then tries to create a new counter.
|
||||
func GetOrRegisterCounter(m Manager, name string) (Counter, error) {
|
||||
counter := m.GetCounter(name)
|
||||
if counter != nil {
|
||||
return counter, nil
|
||||
}
|
||||
|
||||
return m.RegisterCounter(name)
|
||||
}
|
||||
|
||||
// GetOrRegisterOnlineMap tries to get the OnlineMap first. If not exist, it then tries to create a new OnlineMap.
|
||||
func GetOrRegisterOnlineMap(m Manager, name string) (OnlineMap, error) {
|
||||
onlineMap := m.GetOnlineMap(name)
|
||||
if onlineMap != nil {
|
||||
return onlineMap, nil
|
||||
}
|
||||
|
||||
return m.RegisterOnlineMap(name)
|
||||
}
|
||||
|
||||
// GetOrRegisterChannel tries to get the StatChannel first. If not exist, it then tries to create a new channel.
|
||||
func GetOrRegisterChannel(m Manager, name string) (Channel, error) {
|
||||
channel := m.GetChannel(name)
|
||||
if channel != nil {
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
return m.RegisterChannel(name)
|
||||
}
|
||||
|
||||
// ManagerType returns the type of Manager interface. Can be used to implement common.HasType.
|
||||
//
|
||||
// xray:api:stable
|
||||
@@ -160,6 +136,11 @@ func (NoopManager) RegisterCounter(string) (Counter, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GetOrRegisterCounter implements Manager.
|
||||
func (NoopManager) GetOrRegisterCounter(string) (Counter, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// UnregisterCounter implements Manager.
|
||||
func (NoopManager) UnregisterCounter(string) error {
|
||||
return nil
|
||||
@@ -178,6 +159,11 @@ func (NoopManager) RegisterOnlineMap(string) (OnlineMap, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GetOrRegisterOnlineMap implements Manager.
|
||||
func (NoopManager) GetOrRegisterOnlineMap(string) (OnlineMap, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// UnregisterOnlineMap implements Manager.
|
||||
func (NoopManager) UnregisterOnlineMap(string) error {
|
||||
return nil
|
||||
@@ -196,6 +182,11 @@ func (NoopManager) RegisterChannel(string) (Channel, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// GetOrRegisterChannel implements Manager.
|
||||
func (NoopManager) GetOrRegisterChannel(string) (Channel, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
// UnregisterChannel implements Manager.
|
||||
func (NoopManager) UnregisterChannel(string) error {
|
||||
return nil
|
||||
|
||||
@@ -8,12 +8,13 @@ require (
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
|
||||
github.com/golang/mock v1.7.0-rc.1
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/klauspost/cpuid/v2 v2.3.0
|
||||
github.com/klauspost/cpuid/v2 v2.4.0
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/pion/stun/v3 v3.1.6
|
||||
github.com/pires/go-proxyproto v0.12.0
|
||||
github.com/pires/go-proxyproto v0.15.0
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sagernet/sing v0.5.1
|
||||
@@ -22,15 +23,15 @@ require (
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/sync v0.21.0
|
||||
golang.org/x/sys v0.46.0
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
||||
google.golang.org/grpc v1.81.1
|
||||
google.golang.org/grpc v1.82.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
||||
h12.io/socks v1.0.3
|
||||
@@ -51,11 +52,11 @@ require (
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -33,8 +33,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.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
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=
|
||||
@@ -53,8 +53,8 @@ github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8=
|
||||
github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
|
||||
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
|
||||
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
|
||||
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
|
||||
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
|
||||
github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
|
||||
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
|
||||
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=
|
||||
@@ -98,22 +98,22 @@ 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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
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.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
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.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
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.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -121,21 +121,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.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
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=
|
||||
@@ -147,10 +147,10 @@ golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRootEnvAppliesArbitraryValues(t *testing.T) {
|
||||
const (
|
||||
valueKey = "XRAY_TEST_CONFIG_ENV"
|
||||
emptyKey = "XRAY_TEST_CONFIG_EMPTY"
|
||||
)
|
||||
t.Setenv(valueKey, "before")
|
||||
t.Setenv(emptyKey, "before")
|
||||
|
||||
config := new(Config)
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"env": {
|
||||
"XRAY_TEST_CONFIG_ENV": "configured",
|
||||
"XRAY_TEST_CONFIG_EMPTY": ""
|
||||
}
|
||||
}`), config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := config.Build(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got := os.Getenv(valueKey); got != "configured" {
|
||||
t.Fatalf("env %q = %q, want %q", valueKey, got, "configured")
|
||||
}
|
||||
if got := os.Getenv(emptyKey); got != "" {
|
||||
t.Fatalf("env %q = %q, want empty", emptyKey, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvConfigOverride(t *testing.T) {
|
||||
base := EnvConfig{
|
||||
"ONE": "one",
|
||||
"TWO": "old",
|
||||
}
|
||||
override := EnvConfig{
|
||||
"TWO": "new",
|
||||
"THREE": "three",
|
||||
}
|
||||
base.Override(override)
|
||||
|
||||
want := map[string]string{
|
||||
"ONE": "one",
|
||||
"TWO": "new",
|
||||
"THREE": "three",
|
||||
}
|
||||
for key, value := range want {
|
||||
if got := base[key]; got != value {
|
||||
t.Fatalf("env %q = %q, want %q", key, got, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/transport/internet/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type GRPCConfig struct {
|
||||
Authority string `json:"authority"`
|
||||
ServiceName string `json:"serviceName"`
|
||||
MultiMode bool `json:"multiMode"`
|
||||
IdleTimeout int32 `json:"idle_timeout"`
|
||||
HealthCheckTimeout int32 `json:"health_check_timeout"`
|
||||
PermitWithoutStream bool `json:"permit_without_stream"`
|
||||
InitialWindowsSize int32 `json:"initial_windows_size"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
}
|
||||
|
||||
func (g *GRPCConfig) Build() (proto.Message, error) {
|
||||
if g.IdleTimeout <= 0 {
|
||||
g.IdleTimeout = 0
|
||||
}
|
||||
if g.HealthCheckTimeout <= 0 {
|
||||
g.HealthCheckTimeout = 0
|
||||
}
|
||||
if g.InitialWindowsSize < 0 {
|
||||
// default window size of gRPC-go
|
||||
g.InitialWindowsSize = 0
|
||||
}
|
||||
|
||||
return &grpc.Config{
|
||||
Authority: g.Authority,
|
||||
ServiceName: g.ServiceName,
|
||||
MultiMode: g.MultiMode,
|
||||
IdleTimeout: g.IdleTimeout,
|
||||
HealthCheckTimeout: g.HealthCheckTimeout,
|
||||
PermitWithoutStream: g.PermitWithoutStream,
|
||||
InitialWindowsSize: g.InitialWindowsSize,
|
||||
UserAgent: g.UserAgent,
|
||||
}, nil
|
||||
}
|
||||
@@ -24,8 +24,6 @@ func cipherFromString(c string) shadowsocks.CipherType {
|
||||
return shadowsocks.CipherType_CHACHA20_POLY1305
|
||||
case "xchacha20-poly1305", "aead_xchacha20_poly1305", "xchacha20-ietf-poly1305":
|
||||
return shadowsocks.CipherType_XCHACHA20_POLY1305
|
||||
case "none", "plain":
|
||||
return shadowsocks.CipherType_NONE
|
||||
default:
|
||||
return shadowsocks.CipherType_UNKNOWN
|
||||
}
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
package conf
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type NoOpConnectionAuthenticator struct{}
|
||||
|
||||
func (NoOpConnectionAuthenticator) Build() (proto.Message, error) {
|
||||
return new(noop.ConnectionConfig), nil
|
||||
}
|
||||
|
||||
type AuthenticatorRequest struct {
|
||||
Version string `json:"version"`
|
||||
Method string `json:"method"`
|
||||
Path StringList `json:"path"`
|
||||
Headers map[string]*StringList `json:"headers"`
|
||||
}
|
||||
|
||||
func sortMapKeys(m map[string]*StringList) []string {
|
||||
var keys []string
|
||||
for key := range m {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
|
||||
config := &http.RequestConfig{
|
||||
Uri: []string{"/"},
|
||||
Header: []*http.Header{
|
||||
{
|
||||
Name: "Host",
|
||||
Value: []string{"www.baidu.com", "www.bing.com"},
|
||||
},
|
||||
{
|
||||
Name: "User-Agent",
|
||||
Value: []string{utils.ChromeUA},
|
||||
},
|
||||
{
|
||||
Name: "Sec-CH-UA",
|
||||
Value: []string{utils.ChromeUACH},
|
||||
},
|
||||
{
|
||||
Name: "Sec-CH-UA-Mobile",
|
||||
Value: []string{"?0"},
|
||||
},
|
||||
{
|
||||
Name: "Sec-CH-UA-Platform",
|
||||
Value: []string{"Windows"},
|
||||
},
|
||||
{
|
||||
Name: "Sec-Fetch-Mode",
|
||||
Value: []string{"no-cors", "cors", "same-origin"},
|
||||
},
|
||||
{
|
||||
Name: "Sec-Fetch-Dest",
|
||||
Value: []string{"empty"},
|
||||
},
|
||||
{
|
||||
Name: "Sec-Fetch-Site",
|
||||
Value: []string{"none"},
|
||||
},
|
||||
{
|
||||
Name: "Sec-Fetch-User",
|
||||
Value: []string{"?1"},
|
||||
},
|
||||
{
|
||||
Name: "Accept-Encoding",
|
||||
Value: []string{"gzip, deflate"},
|
||||
},
|
||||
{
|
||||
Name: "Connection",
|
||||
Value: []string{"keep-alive"},
|
||||
},
|
||||
{
|
||||
Name: "Pragma",
|
||||
Value: []string{"no-cache"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if len(v.Version) > 0 {
|
||||
config.Version = &http.Version{Value: v.Version}
|
||||
}
|
||||
|
||||
if len(v.Method) > 0 {
|
||||
config.Method = &http.Method{Value: v.Method}
|
||||
}
|
||||
|
||||
if len(v.Path) > 0 {
|
||||
config.Uri = append([]string(nil), v.Path...)
|
||||
}
|
||||
|
||||
if len(v.Headers) > 0 {
|
||||
config.Header = make([]*http.Header, 0, len(v.Headers))
|
||||
headerNames := sortMapKeys(v.Headers)
|
||||
for _, key := range headerNames {
|
||||
value := v.Headers[key]
|
||||
if value == nil {
|
||||
return nil, errors.New("empty HTTP header value: " + key).AtError()
|
||||
}
|
||||
config.Header = append(config.Header, &http.Header{
|
||||
Name: key,
|
||||
Value: append([]string(nil), (*value)...),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type AuthenticatorResponse struct {
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
Headers map[string]*StringList `json:"headers"`
|
||||
}
|
||||
|
||||
func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {
|
||||
config := &http.ResponseConfig{
|
||||
Header: []*http.Header{
|
||||
{
|
||||
Name: "Content-Type",
|
||||
Value: []string{"application/octet-stream", "video/mpeg"},
|
||||
},
|
||||
{
|
||||
Name: "Transfer-Encoding",
|
||||
Value: []string{"chunked"},
|
||||
},
|
||||
{
|
||||
Name: "Connection",
|
||||
Value: []string{"keep-alive"},
|
||||
},
|
||||
{
|
||||
Name: "Pragma",
|
||||
Value: []string{"no-cache"},
|
||||
},
|
||||
{
|
||||
Name: "Cache-Control",
|
||||
Value: []string{"private", "no-cache"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if len(v.Version) > 0 {
|
||||
config.Version = &http.Version{Value: v.Version}
|
||||
}
|
||||
|
||||
if len(v.Status) > 0 || len(v.Reason) > 0 {
|
||||
config.Status = &http.Status{
|
||||
Code: "200",
|
||||
Reason: "OK",
|
||||
}
|
||||
if len(v.Status) > 0 {
|
||||
config.Status.Code = v.Status
|
||||
}
|
||||
if len(v.Reason) > 0 {
|
||||
config.Status.Reason = v.Reason
|
||||
}
|
||||
}
|
||||
|
||||
if len(v.Headers) > 0 {
|
||||
config.Header = make([]*http.Header, 0, len(v.Headers))
|
||||
headerNames := sortMapKeys(v.Headers)
|
||||
for _, key := range headerNames {
|
||||
value := v.Headers[key]
|
||||
if value == nil {
|
||||
return nil, errors.New("empty HTTP header value: " + key).AtError()
|
||||
}
|
||||
config.Header = append(config.Header, &http.Header{
|
||||
Name: key,
|
||||
Value: append([]string(nil), (*value)...),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type Authenticator struct {
|
||||
Request AuthenticatorRequest `json:"request"`
|
||||
Response AuthenticatorResponse `json:"response"`
|
||||
}
|
||||
|
||||
func (v *Authenticator) Build() (proto.Message, error) {
|
||||
config := new(http.Config)
|
||||
requestConfig, err := v.Request.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Request = requestConfig
|
||||
|
||||
responseConfig, err := v.Response.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Response = responseConfig
|
||||
|
||||
return config, nil
|
||||
}
|
||||
@@ -0,0 +1,951 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
googleuuid "github.com/google/uuid"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/noise"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/realm"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/salamander"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/sudoku"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xicmp"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func PraseByteSlice(data json.RawMessage, typ string) ([]byte, error) {
|
||||
switch strings.ToLower(typ) {
|
||||
case "", "array":
|
||||
if len(data) == 0 {
|
||||
return data, nil
|
||||
}
|
||||
var packet []byte
|
||||
if err := json.Unmarshal(data, &packet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return packet, nil
|
||||
case "str":
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(str), nil
|
||||
case "hex":
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hex.DecodeString(str)
|
||||
case "base64":
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return base64.StdEncoding.DecodeString(str)
|
||||
default:
|
||||
return nil, errors.New("unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
|
||||
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
|
||||
"fragment": func() interface{} { return new(FragmentMask) },
|
||||
"sudoku": func() interface{} { return new(Sudoku) },
|
||||
"xmc": func() interface{} { return new(XMC) },
|
||||
}, "type", "settings")
|
||||
|
||||
udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
"header-custom": func() interface{} { return new(HeaderCustomUDP) },
|
||||
"mkcp-legacy": func() interface{} { return new(MkcpLegacy) },
|
||||
"noise": func() interface{} { return new(NoiseMask) },
|
||||
"salamander": func() interface{} { return new(Salamander) },
|
||||
"sudoku": func() interface{} { return new(Sudoku) },
|
||||
"xdns": func() interface{} { return new(Xdns) },
|
||||
"xicmp": func() interface{} { return new(Xicmp) },
|
||||
"realm": func() interface{} { return new(Realm) },
|
||||
}, "type", "settings")
|
||||
)
|
||||
|
||||
type TCPItem struct {
|
||||
Delay Int32Range `json:"delay"`
|
||||
Rand int32 `json:"rand"`
|
||||
RandRange *Int32Range `json:"randRange"`
|
||||
Capture string `json:"capture"`
|
||||
Type string `json:"type"`
|
||||
Reuse string `json:"reuse"`
|
||||
Transform *CustomTransform `json:"transform"`
|
||||
Packet json.RawMessage `json:"packet"`
|
||||
}
|
||||
|
||||
type HeaderCustomTCP struct {
|
||||
Clients [][]TCPItem `json:"clients"`
|
||||
Servers [][]TCPItem `json:"servers"`
|
||||
Errors [][]TCPItem `json:"errors"`
|
||||
}
|
||||
|
||||
func (c *HeaderCustomTCP) Build() (proto.Message, error) {
|
||||
for _, value := range c.Clients {
|
||||
for _, item := range value {
|
||||
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, value := range c.Servers {
|
||||
for _, item := range value {
|
||||
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, value := range c.Errors {
|
||||
for _, item := range value {
|
||||
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errInvalidRange := errors.New("invalid randRange")
|
||||
|
||||
clients := make([]*custom.TCPSequence, len(c.Clients))
|
||||
for i, value := range c.Clients {
|
||||
clients[i] = &custom.TCPSequence{}
|
||||
for _, item := range value {
|
||||
if item.RandRange == nil {
|
||||
item.RandRange = &Int32Range{From: 0, To: 255}
|
||||
}
|
||||
if item.RandRange.From < 0 || item.RandRange.To > 255 {
|
||||
return nil, errInvalidRange
|
||||
}
|
||||
var err error
|
||||
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transform, err := buildCustomTransform(item.Transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clients[i].Sequence = append(clients[i].Sequence, &custom.TCPItem{
|
||||
DelayMin: int64(item.Delay.From),
|
||||
DelayMax: int64(item.Delay.To),
|
||||
Rand: item.Rand,
|
||||
RandMin: item.RandRange.From,
|
||||
RandMax: item.RandRange.To,
|
||||
Packet: item.Packet,
|
||||
Save: item.Capture,
|
||||
Var: item.Reuse,
|
||||
Expr: transform,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
servers := make([]*custom.TCPSequence, len(c.Servers))
|
||||
for i, value := range c.Servers {
|
||||
servers[i] = &custom.TCPSequence{}
|
||||
for _, item := range value {
|
||||
if item.RandRange == nil {
|
||||
item.RandRange = &Int32Range{From: 0, To: 255}
|
||||
}
|
||||
if item.RandRange.From < 0 || item.RandRange.To > 255 {
|
||||
return nil, errInvalidRange
|
||||
}
|
||||
var err error
|
||||
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transform, err := buildCustomTransform(item.Transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
servers[i].Sequence = append(servers[i].Sequence, &custom.TCPItem{
|
||||
DelayMin: int64(item.Delay.From),
|
||||
DelayMax: int64(item.Delay.To),
|
||||
Rand: item.Rand,
|
||||
RandMin: item.RandRange.From,
|
||||
RandMax: item.RandRange.To,
|
||||
Packet: item.Packet,
|
||||
Save: item.Capture,
|
||||
Var: item.Reuse,
|
||||
Expr: transform,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
errors := make([]*custom.TCPSequence, len(c.Errors))
|
||||
for i, value := range c.Errors {
|
||||
errors[i] = &custom.TCPSequence{}
|
||||
for _, item := range value {
|
||||
if item.RandRange == nil {
|
||||
item.RandRange = &Int32Range{From: 0, To: 255}
|
||||
}
|
||||
if item.RandRange.From < 0 || item.RandRange.To > 255 {
|
||||
return nil, errInvalidRange
|
||||
}
|
||||
var err error
|
||||
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transform, err := buildCustomTransform(item.Transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
errors[i].Sequence = append(errors[i].Sequence, &custom.TCPItem{
|
||||
DelayMin: int64(item.Delay.From),
|
||||
DelayMax: int64(item.Delay.To),
|
||||
Rand: item.Rand,
|
||||
RandMin: item.RandRange.From,
|
||||
RandMax: item.RandRange.To,
|
||||
Packet: item.Packet,
|
||||
Save: item.Capture,
|
||||
Var: item.Reuse,
|
||||
Expr: transform,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &custom.TCPConfig{
|
||||
Clients: clients,
|
||||
Servers: servers,
|
||||
Errors: errors,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type FragmentMask struct {
|
||||
Packets string `json:"packets"`
|
||||
Length Int32Range `json:"length"`
|
||||
Delay Int32Range `json:"delay"`
|
||||
Lengths []Int32Range `json:"lengths"`
|
||||
Delays []Int32Range `json:"delays"`
|
||||
MaxSplit Int32Range `json:"maxSplit"`
|
||||
}
|
||||
|
||||
func (c *FragmentMask) Build() (proto.Message, error) {
|
||||
config := &fragment.Config{}
|
||||
|
||||
switch strings.ToLower(c.Packets) {
|
||||
case "tlshello":
|
||||
config.PacketsFrom = 0
|
||||
config.PacketsTo = 1
|
||||
case "":
|
||||
config.PacketsFrom = 0
|
||||
config.PacketsTo = 0
|
||||
default:
|
||||
from, to, err := ParseRangeString(c.Packets)
|
||||
if err != nil {
|
||||
return nil, errors.New("Invalid PacketsFrom").Base(err)
|
||||
}
|
||||
config.PacketsFrom = int64(from)
|
||||
config.PacketsTo = int64(to)
|
||||
if config.PacketsFrom == 0 {
|
||||
return nil, errors.New("PacketsFrom can't be 0")
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Lengths) > 0 {
|
||||
for _, r := range c.Lengths {
|
||||
config.LengthsMin = append(config.LengthsMin, int64(r.From))
|
||||
config.LengthsMax = append(config.LengthsMax, int64(r.To))
|
||||
}
|
||||
} else {
|
||||
config.LengthsMin = append(config.LengthsMin, int64(c.Length.From))
|
||||
config.LengthsMax = append(config.LengthsMax, int64(c.Length.To))
|
||||
}
|
||||
|
||||
if config.LengthsMin[len(config.LengthsMin)-1] == 0 {
|
||||
return nil, errors.New("last lengths entry min can't be 0")
|
||||
}
|
||||
|
||||
if len(c.Delays) > 0 {
|
||||
for _, r := range c.Delays {
|
||||
config.DelaysMin = append(config.DelaysMin, int64(r.From))
|
||||
config.DelaysMax = append(config.DelaysMax, int64(r.To))
|
||||
}
|
||||
} else {
|
||||
config.DelaysMin = append(config.DelaysMin, int64(c.Delay.From))
|
||||
config.DelaysMax = append(config.DelaysMax, int64(c.Delay.To))
|
||||
}
|
||||
|
||||
config.MaxSplitMin = int64(c.MaxSplit.From)
|
||||
config.MaxSplitMax = int64(c.MaxSplit.To)
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type NoiseItem struct {
|
||||
Rand Int32Range `json:"rand"`
|
||||
RandRange *Int32Range `json:"randRange"`
|
||||
Type string `json:"type"`
|
||||
Packet json.RawMessage `json:"packet"`
|
||||
Delay Int32Range `json:"delay"`
|
||||
}
|
||||
|
||||
type NoiseMask struct {
|
||||
Reset Int32Range `json:"reset"`
|
||||
Noise []NoiseItem `json:"noise"`
|
||||
}
|
||||
|
||||
func (c *NoiseMask) Build() (proto.Message, error) {
|
||||
for _, item := range c.Noise {
|
||||
if len(item.Packet) > 0 && item.Rand.To > 0 {
|
||||
return nil, errors.New("len(item.Packet) > 0 && item.Rand.To > 0")
|
||||
}
|
||||
}
|
||||
|
||||
noiseSlice := make([]*noise.Item, 0, len(c.Noise))
|
||||
for _, item := range c.Noise {
|
||||
if item.RandRange == nil {
|
||||
item.RandRange = &Int32Range{From: 0, To: 255}
|
||||
}
|
||||
if item.RandRange.From < 0 || item.RandRange.To > 255 {
|
||||
return nil, errors.New("invalid randRange")
|
||||
}
|
||||
var err error
|
||||
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
noiseSlice = append(noiseSlice, &noise.Item{
|
||||
RandMin: int64(item.Rand.From),
|
||||
RandMax: int64(item.Rand.To),
|
||||
RandRangeMin: item.RandRange.From,
|
||||
RandRangeMax: item.RandRange.To,
|
||||
Packet: item.Packet,
|
||||
DelayMin: int64(item.Delay.From),
|
||||
DelayMax: int64(item.Delay.To),
|
||||
})
|
||||
}
|
||||
|
||||
return &noise.Config{
|
||||
ResetMin: int64(c.Reset.From),
|
||||
ResetMax: int64(c.Reset.To),
|
||||
Items: noiseSlice,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type UDPItem struct {
|
||||
Rand int32 `json:"rand"`
|
||||
RandRange *Int32Range `json:"randRange"`
|
||||
Capture string `json:"capture"`
|
||||
Type string `json:"type"`
|
||||
Reuse string `json:"reuse"`
|
||||
Transform *CustomTransform `json:"transform"`
|
||||
Packet json.RawMessage `json:"packet"`
|
||||
}
|
||||
|
||||
type CustomTransform struct {
|
||||
Op string `json:"op"`
|
||||
Args []CustomTransformArg `json:"args"`
|
||||
}
|
||||
|
||||
type CustomTransformArg struct {
|
||||
Type string `json:"type"`
|
||||
Bytes json.RawMessage `json:"bytes"`
|
||||
U64 *uint64 `json:"u64"`
|
||||
Reuse string `json:"reuse"`
|
||||
Metadata string `json:"metadata"`
|
||||
Transform *CustomTransform `json:"transform"`
|
||||
}
|
||||
|
||||
func validateCustomVarName(name string) error {
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
if !customVarNamePattern.MatchString(name) {
|
||||
return errors.New("invalid variable name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCustomItemSpec(capture string, packet json.RawMessage, rand int32, reuse string, transform *CustomTransform) error {
|
||||
if err := validateCustomVarName(capture); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateCustomVarName(reuse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kindCount := 0
|
||||
if len(packet) > 0 {
|
||||
kindCount++
|
||||
}
|
||||
if rand > 0 {
|
||||
kindCount++
|
||||
}
|
||||
if reuse != "" {
|
||||
kindCount++
|
||||
}
|
||||
if transform != nil {
|
||||
kindCount++
|
||||
}
|
||||
if kindCount > 1 {
|
||||
return errors.New("exactly one item kind must be set")
|
||||
}
|
||||
if kindCount == 0 && capture != "" {
|
||||
return errors.New("exactly one item kind must be set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildCustomTransform(transform *CustomTransform) (*custom.Expr, error) {
|
||||
if transform == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if transform.Op == "" {
|
||||
return nil, errors.New("transform op is required")
|
||||
}
|
||||
if len(transform.Args) == 0 {
|
||||
return nil, errors.New("transform args are required")
|
||||
}
|
||||
|
||||
args := make([]*custom.ExprArg, 0, len(transform.Args))
|
||||
for _, arg := range transform.Args {
|
||||
parsedArg, err := buildCustomTransformArg(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, parsedArg)
|
||||
}
|
||||
|
||||
return &custom.Expr{
|
||||
Op: transform.Op,
|
||||
Args: args,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildCustomTransformArg(arg CustomTransformArg) (*custom.ExprArg, error) {
|
||||
kindCount := 0
|
||||
if len(arg.Bytes) > 0 {
|
||||
kindCount++
|
||||
}
|
||||
if arg.U64 != nil {
|
||||
kindCount++
|
||||
}
|
||||
if arg.Reuse != "" {
|
||||
kindCount++
|
||||
}
|
||||
if arg.Metadata != "" {
|
||||
kindCount++
|
||||
}
|
||||
if arg.Transform != nil {
|
||||
kindCount++
|
||||
}
|
||||
if kindCount != 1 {
|
||||
return nil, errors.New("transform arg must set exactly one value")
|
||||
}
|
||||
|
||||
if len(arg.Bytes) > 0 {
|
||||
value, err := PraseByteSlice(arg.Bytes, arg.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &custom.ExprArg{
|
||||
Value: &custom.ExprArg_Bytes{
|
||||
Bytes: value,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if arg.U64 != nil {
|
||||
return &custom.ExprArg{
|
||||
Value: &custom.ExprArg_U64{
|
||||
U64: *arg.U64,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if arg.Reuse != "" {
|
||||
if err := validateCustomVarName(arg.Reuse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &custom.ExprArg{
|
||||
Value: &custom.ExprArg_Var{
|
||||
Var: arg.Reuse,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
if arg.Metadata != "" {
|
||||
return &custom.ExprArg{
|
||||
Value: &custom.ExprArg_Metadata{
|
||||
Metadata: arg.Metadata,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
parsedExpr, err := buildCustomTransform(arg.Transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &custom.ExprArg{
|
||||
Value: &custom.ExprArg_Expr{
|
||||
Expr: parsedExpr,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type HeaderCustomUDP struct {
|
||||
Mode string `json:"mode"`
|
||||
Client []UDPItem `json:"client"`
|
||||
Server []UDPItem `json:"server"`
|
||||
}
|
||||
|
||||
func (c *HeaderCustomUDP) Build() (proto.Message, error) {
|
||||
switch c.Mode {
|
||||
case "", "prefix", "standalone":
|
||||
default:
|
||||
return nil, errors.New("unknown udp mode")
|
||||
}
|
||||
|
||||
for _, item := range c.Client {
|
||||
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, item := range c.Server {
|
||||
if err := validateCustomItemSpec(item.Capture, item.Packet, item.Rand, item.Reuse, item.Transform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
client := make([]*custom.UDPItem, 0, len(c.Client))
|
||||
for _, item := range c.Client {
|
||||
if item.RandRange == nil {
|
||||
item.RandRange = &Int32Range{From: 0, To: 255}
|
||||
}
|
||||
if item.RandRange.From < 0 || item.RandRange.To > 255 {
|
||||
return nil, errors.New("invalid randRange")
|
||||
}
|
||||
var err error
|
||||
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transform, err := buildCustomTransform(item.Transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client = append(client, &custom.UDPItem{
|
||||
Rand: item.Rand,
|
||||
RandMin: item.RandRange.From,
|
||||
RandMax: item.RandRange.To,
|
||||
Packet: item.Packet,
|
||||
Save: item.Capture,
|
||||
Var: item.Reuse,
|
||||
Expr: transform,
|
||||
})
|
||||
}
|
||||
|
||||
server := make([]*custom.UDPItem, 0, len(c.Server))
|
||||
for _, item := range c.Server {
|
||||
if item.RandRange == nil {
|
||||
item.RandRange = &Int32Range{From: 0, To: 255}
|
||||
}
|
||||
if item.RandRange.From < 0 || item.RandRange.To > 255 {
|
||||
return nil, errors.New("invalid randRange")
|
||||
}
|
||||
var err error
|
||||
if item.Packet, err = PraseByteSlice(item.Packet, item.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transform, err := buildCustomTransform(item.Transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server = append(server, &custom.UDPItem{
|
||||
Rand: item.Rand,
|
||||
RandMin: item.RandRange.From,
|
||||
RandMax: item.RandRange.To,
|
||||
Packet: item.Packet,
|
||||
Save: item.Capture,
|
||||
Var: item.Reuse,
|
||||
Expr: transform,
|
||||
})
|
||||
}
|
||||
|
||||
if c.Mode == "standalone" {
|
||||
return &custom.UDPStandaloneConfig{
|
||||
Client: client,
|
||||
Server: server,
|
||||
}, nil
|
||||
} else {
|
||||
return &custom.UDPConfig{
|
||||
Client: client,
|
||||
Server: server,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type MkcpLegacy struct {
|
||||
Header string `json:"header"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (c *MkcpLegacy) Build() (proto.Message, error) {
|
||||
if len(c.Header) == 0 {
|
||||
if len(c.Value) == 0 {
|
||||
return &original.Config{}, nil
|
||||
} else {
|
||||
return &aes128gcm.Config{Password: c.Value}, nil
|
||||
}
|
||||
}
|
||||
switch strings.ToLower(c.Header) {
|
||||
case "dns":
|
||||
domain := c.Value
|
||||
if len(domain) == 0 {
|
||||
domain = "www.baidu.com"
|
||||
}
|
||||
return &header.Config{ID: 0, Domain: domain}, nil
|
||||
case "dtls":
|
||||
return &header.Config{ID: 1}, nil
|
||||
case "srtp":
|
||||
return &header.Config{ID: 2}, nil
|
||||
case "utp":
|
||||
return &header.Config{ID: 3}, nil
|
||||
case "wechat":
|
||||
return &header.Config{ID: 4}, nil
|
||||
case "wireguard":
|
||||
return &header.Config{ID: 5}, nil
|
||||
default:
|
||||
return nil, errors.New("invalid header ", c.Header)
|
||||
}
|
||||
}
|
||||
|
||||
type Salamander struct {
|
||||
Password string `json:"password"`
|
||||
PacketSize Int32Range `json:"packetSize"`
|
||||
}
|
||||
|
||||
func (c *Salamander) Build() (proto.Message, error) {
|
||||
if c.PacketSize.To > 0 {
|
||||
if c.PacketSize.From <= 0 || c.PacketSize.To > 2048 {
|
||||
return nil, errors.New("gecko: invalid min/max packet size")
|
||||
}
|
||||
return &salamander.GeckoConfig{
|
||||
Password: c.Password,
|
||||
MinPacketSize: c.PacketSize.From,
|
||||
MaxPacketSize: c.PacketSize.To,
|
||||
}, nil
|
||||
}
|
||||
return &salamander.Config{
|
||||
Password: c.Password,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Sudoku struct {
|
||||
Password string `json:"password"`
|
||||
ASCII string `json:"ascii"`
|
||||
|
||||
CustomTable string `json:"customTable"`
|
||||
LegacyCustomTable string `json:"custom_table"`
|
||||
CustomTables []string `json:"customTables"`
|
||||
LegacyCustomSets []string `json:"custom_tables"`
|
||||
|
||||
PaddingMin uint32 `json:"paddingMin"`
|
||||
LegacyPaddingMin uint32 `json:"padding_min"`
|
||||
PaddingMax uint32 `json:"paddingMax"`
|
||||
LegacyPaddingMax uint32 `json:"padding_max"`
|
||||
}
|
||||
|
||||
func (c *Sudoku) Build() (proto.Message, error) {
|
||||
customTable := c.CustomTable
|
||||
if customTable == "" {
|
||||
customTable = c.LegacyCustomTable
|
||||
}
|
||||
customTables := c.CustomTables
|
||||
if len(customTables) == 0 {
|
||||
customTables = c.LegacyCustomSets
|
||||
}
|
||||
|
||||
paddingMin := c.PaddingMin
|
||||
if paddingMin == 0 {
|
||||
paddingMin = c.LegacyPaddingMin
|
||||
}
|
||||
paddingMax := c.PaddingMax
|
||||
if paddingMax == 0 {
|
||||
paddingMax = c.LegacyPaddingMax
|
||||
}
|
||||
|
||||
return &sudoku.Config{
|
||||
Password: c.Password,
|
||||
Ascii: c.ASCII,
|
||||
CustomTable: customTable,
|
||||
CustomTables: customTables,
|
||||
PaddingMin: paddingMin,
|
||||
PaddingMax: paddingMax,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Xdns struct {
|
||||
Domain json.RawMessage `json:"domain"`
|
||||
|
||||
Domains []string `json:"domains"`
|
||||
Resolvers []string `json:"resolvers"`
|
||||
}
|
||||
|
||||
func (c *Xdns) Build() (proto.Message, error) {
|
||||
if c.Domain != nil {
|
||||
return nil, errors.PrintRemovedFeatureError("domain", "domains(server) & resolvers(client)")
|
||||
}
|
||||
|
||||
if len(c.Domains) == 0 && len(c.Resolvers) == 0 {
|
||||
return nil, errors.New("empty domains & empty resolvers")
|
||||
}
|
||||
|
||||
for _, r := range c.Resolvers {
|
||||
if !strings.Contains(r, "+udp://") {
|
||||
return nil, errors.New("invalid resolver ", r)
|
||||
}
|
||||
}
|
||||
|
||||
return &xdns.Config{
|
||||
Domains: c.Domains,
|
||||
Resolvers: c.Resolvers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type XMC struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Profiles []XMCProfile `json:"profiles"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type XMCProfile struct {
|
||||
// Resolve the UUID by username, then request the session profile with
|
||||
// unsigned=false. Client and server must use the same signed profile.
|
||||
Username string `json:"username"`
|
||||
UUID string `json:"uuid"`
|
||||
TexturesValue string `json:"texturesValue"`
|
||||
TexturesSignature string `json:"texturesSignature"`
|
||||
}
|
||||
|
||||
var xmcUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9_]{3,16}$`)
|
||||
|
||||
func (c *XMCProfile) Build() (*xmc.Profile, error) {
|
||||
if !xmcUsernamePattern.MatchString(c.Username) {
|
||||
return nil, fmt.Errorf("invalid minecraft profile username: %q", c.Username)
|
||||
}
|
||||
|
||||
profileUUID, err := googleuuid.Parse(c.UUID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid minecraft profile UUID: %w", err)
|
||||
}
|
||||
if c.TexturesValue == "" || c.TexturesSignature == "" {
|
||||
return nil, fmt.Errorf("incomplete minecraft profile textures")
|
||||
}
|
||||
|
||||
return &xmc.Profile{
|
||||
Username: c.Username,
|
||||
Uuid: append([]byte(nil), profileUUID[:]...),
|
||||
TexturesValue: c.TexturesValue,
|
||||
TexturesSignature: c.TexturesSignature,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *XMC) Build() (proto.Message, error) {
|
||||
if len(c.Profiles) == 0 {
|
||||
return nil, fmt.Errorf("minecraft profiles are required")
|
||||
}
|
||||
|
||||
if c.Password == "" {
|
||||
return nil, fmt.Errorf("empty password")
|
||||
}
|
||||
|
||||
rsaPrivateKey, err := xmc.DeriveRSAKey(c.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive minecraft rsa key: %w", err)
|
||||
}
|
||||
|
||||
rsaPublicKey, err := x509.MarshalPKIXPublicKey(&rsaPrivateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal minecraft rsa public key: %w", err)
|
||||
}
|
||||
|
||||
profiles := make([]*xmc.Profile, 0, len(c.Profiles))
|
||||
for i := range c.Profiles {
|
||||
profile, err := c.Profiles[i].Build()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build minecraft profile %d: %w", i, err)
|
||||
}
|
||||
profiles = append(profiles, profile)
|
||||
}
|
||||
|
||||
return &xmc.Config{
|
||||
Password: c.Password,
|
||||
Hostname: c.Hostname,
|
||||
RsaPrivateKey: x509.MarshalPKCS1PrivateKey(rsaPrivateKey),
|
||||
RsaPublicKey: rsaPublicKey,
|
||||
Profiles: profiles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Xicmp struct {
|
||||
DGRAM bool `json:"dgram"`
|
||||
IPs []string `json:"ips"`
|
||||
}
|
||||
|
||||
func (c *Xicmp) Build() (proto.Message, error) {
|
||||
for _, ip := range c.IPs {
|
||||
if _, err := netip.ParseAddr(ip); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
config := &xicmp.Config{
|
||||
DGRAM: c.DGRAM,
|
||||
IPs: c.IPs,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type Realm struct {
|
||||
Url string `json:"url"`
|
||||
StunServers []string `json:"stunServers"`
|
||||
TlsConfig *TLSConfig `json:"tlsConfig"`
|
||||
}
|
||||
|
||||
func (c *Realm) Build() (proto.Message, error) {
|
||||
var scheme, host, port, token, id string
|
||||
var stunServers []string
|
||||
var tlsConfig *tls.Config
|
||||
|
||||
u, err := url.Parse(c.Url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "realm":
|
||||
scheme = "https"
|
||||
case "realm+http":
|
||||
scheme = "http"
|
||||
default:
|
||||
return nil, errors.New("invalid scheme", u.Scheme)
|
||||
}
|
||||
|
||||
host = u.Hostname()
|
||||
if host == "" {
|
||||
return nil, errors.New("invalid host", host)
|
||||
}
|
||||
|
||||
port = u.Port()
|
||||
if port == "" {
|
||||
port = "443"
|
||||
if scheme == "http" {
|
||||
port = "80"
|
||||
}
|
||||
}
|
||||
|
||||
token, err = url.PathUnescape(u.User.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token == "" {
|
||||
return nil, errors.New("invalid token", token)
|
||||
}
|
||||
|
||||
id, err = url.PathUnescape(strings.TrimPrefix(u.EscapedPath(), "/"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if id == "" {
|
||||
return nil, errors.New("invalid id", id)
|
||||
}
|
||||
|
||||
if len(c.StunServers) == 0 {
|
||||
return nil, errors.New("empty stunServers")
|
||||
}
|
||||
|
||||
for _, s := range c.StunServers {
|
||||
_, _, err = net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
stunServers = c.StunServers
|
||||
|
||||
if c.TlsConfig != nil {
|
||||
tc, err := c.TlsConfig.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig = tc.(*tls.Config)
|
||||
}
|
||||
|
||||
return &realm.Config{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Port: port,
|
||||
Token: token,
|
||||
ID: id,
|
||||
StunServers: stunServers,
|
||||
TlsConfig: tlsConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Mask struct {
|
||||
Type string `json:"type"`
|
||||
Settings *json.RawMessage `json:"settings"`
|
||||
}
|
||||
|
||||
func (c *Mask) Build(tcp bool) (proto.Message, error) {
|
||||
loader := udpmaskLoader
|
||||
if tcp {
|
||||
loader = tcpmaskLoader
|
||||
}
|
||||
|
||||
settings := []byte("{}")
|
||||
if c.Settings != nil {
|
||||
settings = ([]byte)(*c.Settings)
|
||||
}
|
||||
rawConfig, err := loader.LoadWithID(settings, c.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts, err := rawConfig.(Buildable).Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ts, nil
|
||||
}
|
||||
|
||||
type QuicParamsConfig struct {
|
||||
Congestion string `json:"congestion"`
|
||||
Debug bool `json:"debug"`
|
||||
BbrProfile string `json:"bbrProfile"`
|
||||
BrutalUp Bandwidth `json:"brutalUp"`
|
||||
BrutalDown Bandwidth `json:"brutalDown"`
|
||||
UdpHop UdpHop `json:"udpHop"`
|
||||
InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"`
|
||||
MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"`
|
||||
InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"`
|
||||
MaxConnectionReceiveWindow uint64 `json:"maxConnectionReceiveWindow"`
|
||||
MaxIdleTimeout int64 `json:"maxIdleTimeout"`
|
||||
KeepAlivePeriod int64 `json:"keepAlivePeriod"`
|
||||
DisablePathMTUDiscovery bool `json:"disablePathMTUDiscovery"`
|
||||
MaxIncomingStreams int64 `json:"maxIncomingStreams"`
|
||||
}
|
||||
|
||||
type FinalMask struct {
|
||||
Tcp []Mask `json:"tcp"`
|
||||
Udp []Mask `json:"udp"`
|
||||
QuicParams *QuicParamsConfig `json:"quicParams"`
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
|
||||
)
|
||||
|
||||
func TestXMCBuildProfile(t *testing.T) {
|
||||
built, err := (&XMC{
|
||||
Password: "test-password",
|
||||
Profiles: []XMCProfile{
|
||||
{
|
||||
Username: "TestUser",
|
||||
UUID: "00112233-4455-6677-8899-aabbccddeeff",
|
||||
TexturesValue: "textures-value",
|
||||
TexturesSignature: "textures-signature",
|
||||
},
|
||||
},
|
||||
}).Build()
|
||||
if err != nil {
|
||||
t.Fatalf("build XMC config: %v", err)
|
||||
}
|
||||
config := built.(*xmc.Config)
|
||||
if len(config.Profiles) != 1 || len(config.Profiles[0].Uuid) != 16 {
|
||||
t.Fatalf("unexpected profiles: %+v", config.Profiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXMCBuildRequiresProfile(t *testing.T) {
|
||||
_, err := (&XMC{Password: "test-password"}).Build()
|
||||
if err == nil || !strings.Contains(err.Error(), "profiles are required") {
|
||||
t.Fatalf("expected required profiles error, got %v", err)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,814 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/platform/filesystem"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/grpc"
|
||||
"github.com/xtls/xray-core/transport/internet/headers/http"
|
||||
"github.com/xtls/xray-core/transport/internet/headers/noop"
|
||||
"github.com/xtls/xray-core/transport/internet/httpupgrade"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria"
|
||||
"github.com/xtls/xray-core/transport/internet/kcp"
|
||||
"github.com/xtls/xray-core/transport/internet/splithttp"
|
||||
"github.com/xtls/xray-core/transport/internet/tcp"
|
||||
"github.com/xtls/xray-core/transport/internet/websocket"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type NoOpConnectionAuthenticator struct{}
|
||||
|
||||
func (NoOpConnectionAuthenticator) Build() (proto.Message, error) {
|
||||
return new(noop.ConnectionConfig), nil
|
||||
}
|
||||
|
||||
type AuthenticatorRequest struct {
|
||||
Version string `json:"version"`
|
||||
Method string `json:"method"`
|
||||
Path StringList `json:"path"`
|
||||
Headers map[string]*StringList `json:"headers"`
|
||||
}
|
||||
|
||||
func sortMapKeys(m map[string]*StringList) []string {
|
||||
var keys []string
|
||||
for key := range m {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
|
||||
config := &http.RequestConfig{
|
||||
Uri: []string{"/"},
|
||||
Header: []*http.Header{
|
||||
{
|
||||
Name: "Host",
|
||||
Value: []string{"www.baidu.com", "www.bing.com"},
|
||||
},
|
||||
{
|
||||
Name: "User-Agent",
|
||||
Value: []string{utils.ChromeUA},
|
||||
},
|
||||
{
|
||||
Name: "Sec-CH-UA",
|
||||
Value: []string{utils.ChromeUACH},
|
||||
},
|
||||
{
|
||||
Name: "Sec-CH-UA-Mobile",
|
||||
Value: []string{"?0"},
|
||||
},
|
||||
{
|
||||
Name: "Sec-CH-UA-Platform",
|
||||
Value: []string{"Windows"},
|
||||
},
|
||||
{
|
||||
Name: "Sec-Fetch-Mode",
|
||||
Value: []string{"no-cors", "cors", "same-origin"},
|
||||
},
|
||||
{
|
||||
Name: "Sec-Fetch-Dest",
|
||||
Value: []string{"empty"},
|
||||
},
|
||||
{
|
||||
Name: "Sec-Fetch-Site",
|
||||
Value: []string{"none"},
|
||||
},
|
||||
{
|
||||
Name: "Sec-Fetch-User",
|
||||
Value: []string{"?1"},
|
||||
},
|
||||
{
|
||||
Name: "Accept-Encoding",
|
||||
Value: []string{"gzip, deflate"},
|
||||
},
|
||||
{
|
||||
Name: "Connection",
|
||||
Value: []string{"keep-alive"},
|
||||
},
|
||||
{
|
||||
Name: "Pragma",
|
||||
Value: []string{"no-cache"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if len(v.Version) > 0 {
|
||||
config.Version = &http.Version{Value: v.Version}
|
||||
}
|
||||
|
||||
if len(v.Method) > 0 {
|
||||
config.Method = &http.Method{Value: v.Method}
|
||||
}
|
||||
|
||||
if len(v.Path) > 0 {
|
||||
config.Uri = append([]string(nil), v.Path...)
|
||||
}
|
||||
|
||||
if len(v.Headers) > 0 {
|
||||
config.Header = make([]*http.Header, 0, len(v.Headers))
|
||||
headerNames := sortMapKeys(v.Headers)
|
||||
for _, key := range headerNames {
|
||||
value := v.Headers[key]
|
||||
if value == nil {
|
||||
return nil, errors.New("empty HTTP header value: " + key).AtError()
|
||||
}
|
||||
config.Header = append(config.Header, &http.Header{
|
||||
Name: key,
|
||||
Value: append([]string(nil), (*value)...),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type AuthenticatorResponse struct {
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
Headers map[string]*StringList `json:"headers"`
|
||||
}
|
||||
|
||||
func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {
|
||||
config := &http.ResponseConfig{
|
||||
Header: []*http.Header{
|
||||
{
|
||||
Name: "Content-Type",
|
||||
Value: []string{"application/octet-stream", "video/mpeg"},
|
||||
},
|
||||
{
|
||||
Name: "Transfer-Encoding",
|
||||
Value: []string{"chunked"},
|
||||
},
|
||||
{
|
||||
Name: "Connection",
|
||||
Value: []string{"keep-alive"},
|
||||
},
|
||||
{
|
||||
Name: "Pragma",
|
||||
Value: []string{"no-cache"},
|
||||
},
|
||||
{
|
||||
Name: "Cache-Control",
|
||||
Value: []string{"private", "no-cache"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if len(v.Version) > 0 {
|
||||
config.Version = &http.Version{Value: v.Version}
|
||||
}
|
||||
|
||||
if len(v.Status) > 0 || len(v.Reason) > 0 {
|
||||
config.Status = &http.Status{
|
||||
Code: "200",
|
||||
Reason: "OK",
|
||||
}
|
||||
if len(v.Status) > 0 {
|
||||
config.Status.Code = v.Status
|
||||
}
|
||||
if len(v.Reason) > 0 {
|
||||
config.Status.Reason = v.Reason
|
||||
}
|
||||
}
|
||||
|
||||
if len(v.Headers) > 0 {
|
||||
config.Header = make([]*http.Header, 0, len(v.Headers))
|
||||
headerNames := sortMapKeys(v.Headers)
|
||||
for _, key := range headerNames {
|
||||
value := v.Headers[key]
|
||||
if value == nil {
|
||||
return nil, errors.New("empty HTTP header value: " + key).AtError()
|
||||
}
|
||||
config.Header = append(config.Header, &http.Header{
|
||||
Name: key,
|
||||
Value: append([]string(nil), (*value)...),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type Authenticator struct {
|
||||
Request AuthenticatorRequest `json:"request"`
|
||||
Response AuthenticatorResponse `json:"response"`
|
||||
}
|
||||
|
||||
func (v *Authenticator) Build() (proto.Message, error) {
|
||||
config := new(http.Config)
|
||||
requestConfig, err := v.Request.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Request = requestConfig
|
||||
|
||||
responseConfig, err := v.Response.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Response = responseConfig
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
var tcpHeaderLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
"none": func() interface{} { return new(NoOpConnectionAuthenticator) },
|
||||
"http": func() interface{} { return new(Authenticator) },
|
||||
}, "type", "")
|
||||
|
||||
type TCPConfig struct {
|
||||
HeaderConfig json.RawMessage `json:"header"`
|
||||
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *TCPConfig) Build() (proto.Message, error) {
|
||||
config := new(tcp.Config)
|
||||
if len(c.HeaderConfig) > 0 {
|
||||
headerConfig, _, err := tcpHeaderLoader.Load(c.HeaderConfig)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid TCP header config").Base(err).AtError()
|
||||
}
|
||||
ts, err := headerConfig.(Buildable).Build()
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid TCP header config").Base(err).AtError()
|
||||
}
|
||||
config.HeaderSettings = serial.ToTypedMessage(ts)
|
||||
}
|
||||
if c.AcceptProxyProtocol {
|
||||
config.AcceptProxyProtocol = c.AcceptProxyProtocol
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type SplitHTTPConfig struct {
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Mode string `json:"mode"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
XPaddingBytes Int32Range `json:"xPaddingBytes"`
|
||||
XPaddingObfsMode bool `json:"xPaddingObfsMode"`
|
||||
XPaddingKey string `json:"xPaddingKey"`
|
||||
XPaddingHeader string `json:"xPaddingHeader"`
|
||||
XPaddingPlacement string `json:"xPaddingPlacement"`
|
||||
XPaddingMethod string `json:"xPaddingMethod"`
|
||||
UplinkHTTPMethod string `json:"uplinkHTTPMethod"`
|
||||
SessionIDPlacement string `json:"sessionIDPlacement"`
|
||||
SessionIDKey string `json:"sessionIDKey"`
|
||||
SessionIDTable string `json:"sessionIDTable"`
|
||||
SessionIDLength Int32Range `json:"sessionIDLength"`
|
||||
SeqPlacement string `json:"seqPlacement"`
|
||||
SeqKey string `json:"seqKey"`
|
||||
UplinkDataPlacement string `json:"uplinkDataPlacement"`
|
||||
UplinkDataKey string `json:"uplinkDataKey"`
|
||||
UplinkChunkSize Int32Range `json:"uplinkChunkSize"`
|
||||
NoGRPCHeader bool `json:"noGRPCHeader"`
|
||||
NoSSEHeader bool `json:"noSSEHeader"`
|
||||
ScMaxEachPostBytes Int32Range `json:"scMaxEachPostBytes"`
|
||||
ScMinPostsIntervalMs Int32Range `json:"scMinPostsIntervalMs"`
|
||||
ScMaxBufferedPosts int64 `json:"scMaxBufferedPosts"`
|
||||
ScStreamUpServerSecs Int32Range `json:"scStreamUpServerSecs"`
|
||||
ServerMaxHeaderBytes int32 `json:"serverMaxHeaderBytes"`
|
||||
Xmux XmuxConfig `json:"xmux"`
|
||||
DownloadSettings *StreamConfig `json:"downloadSettings"`
|
||||
Extra json.RawMessage `json:"extra"`
|
||||
}
|
||||
|
||||
type XmuxConfig struct {
|
||||
MaxConcurrency Int32Range `json:"maxConcurrency"`
|
||||
MaxConnections Int32Range `json:"maxConnections"`
|
||||
CMaxReuseTimes Int32Range `json:"cMaxReuseTimes"`
|
||||
HMaxRequestTimes Int32Range `json:"hMaxRequestTimes"`
|
||||
HMaxReusableSecs Int32Range `json:"hMaxReusableSecs"`
|
||||
HKeepAlivePeriod int64 `json:"hKeepAlivePeriod"`
|
||||
}
|
||||
|
||||
func newRangeConfig(input Int32Range) *splithttp.RangeConfig {
|
||||
return &splithttp.RangeConfig{
|
||||
From: input.From,
|
||||
To: input.To,
|
||||
}
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *SplitHTTPConfig) Build() (proto.Message, error) {
|
||||
if c.Extra != nil {
|
||||
var extra SplitHTTPConfig
|
||||
if err := json.Unmarshal(c.Extra, &extra); err != nil {
|
||||
return nil, errors.New(`Failed to unmarshal "extra".`).Base(err)
|
||||
}
|
||||
extra.Host = c.Host
|
||||
extra.Path = c.Path
|
||||
extra.Mode = c.Mode
|
||||
c = &extra
|
||||
}
|
||||
|
||||
switch c.Mode {
|
||||
case "":
|
||||
c.Mode = "auto"
|
||||
case "auto", "packet-up", "stream-up", "stream-one":
|
||||
default:
|
||||
return nil, errors.New("unsupported mode: " + c.Mode)
|
||||
}
|
||||
|
||||
// Priority (client): host > serverName > address
|
||||
for k := range c.Headers {
|
||||
if strings.ToLower(k) == "host" {
|
||||
return nil, errors.New(`"headers" can't contain "host"`)
|
||||
}
|
||||
}
|
||||
|
||||
if c.XPaddingBytes != (Int32Range{}) && (c.XPaddingBytes.From <= 0 || c.XPaddingBytes.To <= 0) {
|
||||
return nil, errors.New("xPaddingBytes cannot be disabled")
|
||||
}
|
||||
|
||||
if c.XPaddingKey == "" {
|
||||
c.XPaddingKey = "x_padding"
|
||||
}
|
||||
|
||||
if c.XPaddingHeader == "" {
|
||||
c.XPaddingHeader = "X-Padding"
|
||||
}
|
||||
|
||||
switch c.XPaddingPlacement {
|
||||
case "":
|
||||
c.XPaddingPlacement = "queryInHeader"
|
||||
case "cookie", "header", "query", "queryInHeader":
|
||||
default:
|
||||
return nil, errors.New("unsupported padding placement: " + c.XPaddingPlacement)
|
||||
}
|
||||
|
||||
switch c.XPaddingMethod {
|
||||
case "":
|
||||
c.XPaddingMethod = "repeat-x"
|
||||
case "repeat-x", "tokenish":
|
||||
default:
|
||||
return nil, errors.New("unsupported padding method: " + c.XPaddingMethod)
|
||||
}
|
||||
|
||||
switch c.UplinkDataPlacement {
|
||||
case "":
|
||||
c.UplinkDataPlacement = splithttp.PlacementAuto
|
||||
case splithttp.PlacementAuto, splithttp.PlacementBody:
|
||||
case splithttp.PlacementCookie, splithttp.PlacementHeader:
|
||||
if c.Mode != "packet-up" {
|
||||
return nil, errors.New("UplinkDataPlacement can be " + c.UplinkDataPlacement + " only in packet-up mode")
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported uplink data placement: " + c.UplinkDataPlacement)
|
||||
}
|
||||
|
||||
if c.UplinkHTTPMethod == "" {
|
||||
c.UplinkHTTPMethod = "POST"
|
||||
}
|
||||
c.UplinkHTTPMethod = strings.ToUpper(c.UplinkHTTPMethod)
|
||||
|
||||
if c.UplinkHTTPMethod == "GET" && c.Mode != "packet-up" {
|
||||
return nil, errors.New("uplinkHTTPMethod can be GET only in packet-up mode")
|
||||
}
|
||||
|
||||
switch c.SessionIDPlacement {
|
||||
case "":
|
||||
c.SessionIDPlacement = "path"
|
||||
case "path", "cookie", "header", "query":
|
||||
default:
|
||||
return nil, errors.New("unsupported session placement: " + c.SessionIDPlacement)
|
||||
}
|
||||
|
||||
switch c.SeqPlacement {
|
||||
case "":
|
||||
c.SeqPlacement = "path"
|
||||
case "path", "cookie", "header", "query":
|
||||
default:
|
||||
return nil, errors.New("unsupported seq placement: " + c.SeqPlacement)
|
||||
}
|
||||
|
||||
if c.SessionIDPlacement != "path" && c.SessionIDKey == "" {
|
||||
switch c.SessionIDPlacement {
|
||||
case "cookie", "query":
|
||||
c.SessionIDKey = "x_session"
|
||||
case "header":
|
||||
c.SessionIDKey = "X-Session"
|
||||
}
|
||||
}
|
||||
|
||||
if c.SessionIDTable != "" {
|
||||
if predefined, ok := splithttp.PredefinedTable[c.SessionIDTable]; ok {
|
||||
c.SessionIDTable = predefined
|
||||
}
|
||||
room := roomSize(len(c.SessionIDTable), c.SessionIDLength.From, c.SessionIDLength.To)
|
||||
// 2.1B possiblities should be enough
|
||||
if room.Cmp(big.NewInt(2<<30)) < 0 {
|
||||
return nil, errors.New("sessionIDTable or sessionIDLength is too small")
|
||||
}
|
||||
if c.SessionIDLength.From <= 0 {
|
||||
return nil, errors.New("sessionIDLength.from must be greater than 0")
|
||||
}
|
||||
for i := 0; i < len(c.SessionIDTable); i++ {
|
||||
if c.SessionIDTable[i] >= 0x80 {
|
||||
return nil, errors.New("sessionIDTable must contain only ASCII characters")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.SeqPlacement != "path" && c.SeqKey == "" {
|
||||
switch c.SeqPlacement {
|
||||
case "cookie", "query":
|
||||
c.SeqKey = "x_seq"
|
||||
case "header":
|
||||
c.SeqKey = "X-Seq"
|
||||
}
|
||||
}
|
||||
|
||||
if c.UplinkDataPlacement != splithttp.PlacementBody && c.UplinkDataKey == "" {
|
||||
switch c.UplinkDataPlacement {
|
||||
case splithttp.PlacementCookie:
|
||||
c.UplinkDataKey = "x_data"
|
||||
case splithttp.PlacementAuto, splithttp.PlacementHeader:
|
||||
c.UplinkDataKey = "X-Data"
|
||||
}
|
||||
}
|
||||
|
||||
if c.ServerMaxHeaderBytes < 0 {
|
||||
return nil, errors.New("invalid negative value of maxHeaderBytes")
|
||||
}
|
||||
|
||||
if c.Xmux.MaxConnections.To > 0 && c.Xmux.MaxConcurrency.To > 0 {
|
||||
return nil, errors.New("maxConnections cannot be specified together with maxConcurrency")
|
||||
}
|
||||
if c.Xmux == (XmuxConfig{}) {
|
||||
c.Xmux.MaxConnections.From = 3
|
||||
c.Xmux.MaxConnections.To = 3
|
||||
c.Xmux.HMaxRequestTimes.From = 600
|
||||
c.Xmux.HMaxRequestTimes.To = 900
|
||||
c.Xmux.HMaxReusableSecs.From = 1800
|
||||
c.Xmux.HMaxReusableSecs.To = 3000
|
||||
}
|
||||
|
||||
config := &splithttp.Config{
|
||||
Host: c.Host,
|
||||
Path: c.Path,
|
||||
Mode: c.Mode,
|
||||
Headers: c.Headers,
|
||||
XPaddingBytes: newRangeConfig(c.XPaddingBytes),
|
||||
XPaddingObfsMode: c.XPaddingObfsMode,
|
||||
XPaddingKey: c.XPaddingKey,
|
||||
XPaddingHeader: c.XPaddingHeader,
|
||||
XPaddingPlacement: c.XPaddingPlacement,
|
||||
XPaddingMethod: c.XPaddingMethod,
|
||||
UplinkHTTPMethod: c.UplinkHTTPMethod,
|
||||
SessionIDPlacement: c.SessionIDPlacement,
|
||||
SeqPlacement: c.SeqPlacement,
|
||||
SessionIDKey: c.SessionIDKey,
|
||||
SeqKey: c.SeqKey,
|
||||
UplinkDataPlacement: c.UplinkDataPlacement,
|
||||
UplinkDataKey: c.UplinkDataKey,
|
||||
UplinkChunkSize: newRangeConfig(c.UplinkChunkSize),
|
||||
NoGRPCHeader: c.NoGRPCHeader,
|
||||
NoSSEHeader: c.NoSSEHeader,
|
||||
ScMaxEachPostBytes: newRangeConfig(c.ScMaxEachPostBytes),
|
||||
ScMinPostsIntervalMs: newRangeConfig(c.ScMinPostsIntervalMs),
|
||||
ScMaxBufferedPosts: c.ScMaxBufferedPosts,
|
||||
ScStreamUpServerSecs: newRangeConfig(c.ScStreamUpServerSecs),
|
||||
ServerMaxHeaderBytes: c.ServerMaxHeaderBytes,
|
||||
SessionIDTable: c.SessionIDTable,
|
||||
SessionIDLength: newRangeConfig(c.SessionIDLength),
|
||||
Xmux: &splithttp.XmuxConfig{
|
||||
MaxConcurrency: newRangeConfig(c.Xmux.MaxConcurrency),
|
||||
MaxConnections: newRangeConfig(c.Xmux.MaxConnections),
|
||||
CMaxReuseTimes: newRangeConfig(c.Xmux.CMaxReuseTimes),
|
||||
HMaxRequestTimes: newRangeConfig(c.Xmux.HMaxRequestTimes),
|
||||
HMaxReusableSecs: newRangeConfig(c.Xmux.HMaxReusableSecs),
|
||||
HKeepAlivePeriod: c.Xmux.HKeepAlivePeriod,
|
||||
},
|
||||
}
|
||||
|
||||
if c.DownloadSettings != nil {
|
||||
if c.Mode == "stream-one" {
|
||||
return nil, errors.New(`Can not use "downloadSettings" in "stream-one" mode.`)
|
||||
}
|
||||
var err error
|
||||
if config.DownloadSettings, err = c.DownloadSettings.Build(); err != nil {
|
||||
return nil, errors.New(`Failed to build "downloadSettings".`).Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func roomSize(tableSize int, min, max int32) *big.Int {
|
||||
base := big.NewInt(int64(tableSize))
|
||||
sum := new(big.Int)
|
||||
term := new(big.Int)
|
||||
for k := min; k <= max; k++ {
|
||||
term.Exp(base, big.NewInt(int64(k)), nil)
|
||||
sum.Add(sum, term)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
type KCPConfig struct {
|
||||
Mtu *uint32 `json:"mtu"`
|
||||
Tti *uint32 `json:"tti"`
|
||||
UpCap *uint32 `json:"uplinkCapacity"`
|
||||
DownCap *uint32 `json:"downlinkCapacity"`
|
||||
CwndMultiplier *uint32 `json:"cwndMultiplier"`
|
||||
MaxSendingWindow *uint32 `json:"maxSendingWindow"`
|
||||
|
||||
HeaderConfig json.RawMessage `json:"header"`
|
||||
Seed *string `json:"seed"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *KCPConfig) Build() (proto.Message, error) {
|
||||
if c.HeaderConfig != nil || c.Seed != nil {
|
||||
return nil, errors.PrintRemovedFeatureError("mkcp header & seed", "finalmask/udp header-* & mkcp-original & mkcp-aes128gcm")
|
||||
}
|
||||
|
||||
config := common.Must2(internet.CreateTransportConfig(kcp.ProtocolName)).(*kcp.Config)
|
||||
|
||||
if c.Mtu != nil {
|
||||
config.Mtu = *c.Mtu
|
||||
}
|
||||
if c.Tti != nil {
|
||||
config.Tti = *c.Tti
|
||||
}
|
||||
if c.UpCap != nil {
|
||||
config.UplinkCapacity = *c.UpCap
|
||||
}
|
||||
if c.DownCap != nil {
|
||||
config.DownlinkCapacity = *c.DownCap
|
||||
}
|
||||
if c.CwndMultiplier != nil {
|
||||
config.CwndMultiplier = *c.CwndMultiplier
|
||||
}
|
||||
if c.MaxSendingWindow != nil {
|
||||
config.MaxSendingWindow = *c.MaxSendingWindow
|
||||
}
|
||||
|
||||
if config.Mtu < 21 {
|
||||
return nil, errors.New("Mtu must be at least 21").AtError()
|
||||
}
|
||||
if config.Tti < 10 || config.Tti > 1000 {
|
||||
return nil, errors.New("invalid mKCP TTI: ", c.Tti).AtError()
|
||||
}
|
||||
if config.CwndMultiplier < 1 {
|
||||
return nil, errors.New("CwndMultiplier must be at least 1").AtError()
|
||||
}
|
||||
if config.GetSendingBufferSize() == 0 {
|
||||
return nil, errors.New("MaxSendingWindow must be >= Mtu").AtError()
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type GRPCConfig struct {
|
||||
Authority string `json:"authority"`
|
||||
ServiceName string `json:"serviceName"`
|
||||
MultiMode bool `json:"multiMode"`
|
||||
IdleTimeout int32 `json:"idle_timeout"`
|
||||
HealthCheckTimeout int32 `json:"health_check_timeout"`
|
||||
PermitWithoutStream bool `json:"permit_without_stream"`
|
||||
InitialWindowsSize int32 `json:"initial_windows_size"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
}
|
||||
|
||||
func (g *GRPCConfig) Build() (proto.Message, error) {
|
||||
if g.IdleTimeout <= 0 {
|
||||
g.IdleTimeout = 0
|
||||
}
|
||||
if g.HealthCheckTimeout <= 0 {
|
||||
g.HealthCheckTimeout = 0
|
||||
}
|
||||
if g.InitialWindowsSize < 0 {
|
||||
// default window size of gRPC-go
|
||||
g.InitialWindowsSize = 0
|
||||
}
|
||||
|
||||
return &grpc.Config{
|
||||
Authority: g.Authority,
|
||||
ServiceName: g.ServiceName,
|
||||
MultiMode: g.MultiMode,
|
||||
IdleTimeout: g.IdleTimeout,
|
||||
HealthCheckTimeout: g.HealthCheckTimeout,
|
||||
PermitWithoutStream: g.PermitWithoutStream,
|
||||
InitialWindowsSize: g.InitialWindowsSize,
|
||||
UserAgent: g.UserAgent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type WebSocketConfig struct {
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
|
||||
HeartbeatPeriod uint32 `json:"heartbeatPeriod"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *WebSocketConfig) Build() (proto.Message, error) {
|
||||
path := c.Path
|
||||
var ed uint32
|
||||
if u, err := url.Parse(path); err == nil {
|
||||
if q := u.Query(); q.Get("ed") != "" {
|
||||
Ed, _ := strconv.Atoi(q.Get("ed"))
|
||||
ed = uint32(Ed)
|
||||
q.Del("ed")
|
||||
u.RawQuery = q.Encode()
|
||||
path = u.String()
|
||||
}
|
||||
}
|
||||
// Priority (client): host > serverName > address
|
||||
for k, v := range c.Headers {
|
||||
if strings.ToLower(k) == "host" {
|
||||
errors.PrintDeprecatedFeatureWarning(`"host" in "headers"`, `independent "host"`)
|
||||
if c.Host == "" {
|
||||
c.Host = v
|
||||
}
|
||||
delete(c.Headers, k)
|
||||
}
|
||||
}
|
||||
config := &websocket.Config{
|
||||
Path: path,
|
||||
Host: c.Host,
|
||||
Header: c.Headers,
|
||||
AcceptProxyProtocol: c.AcceptProxyProtocol,
|
||||
Ed: ed,
|
||||
HeartbeatPeriod: c.HeartbeatPeriod,
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type HttpUpgradeConfig struct {
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *HttpUpgradeConfig) Build() (proto.Message, error) {
|
||||
path := c.Path
|
||||
var ed uint32
|
||||
if u, err := url.Parse(path); err == nil {
|
||||
if q := u.Query(); q.Get("ed") != "" {
|
||||
Ed, _ := strconv.Atoi(q.Get("ed"))
|
||||
ed = uint32(Ed)
|
||||
q.Del("ed")
|
||||
u.RawQuery = q.Encode()
|
||||
path = u.String()
|
||||
}
|
||||
}
|
||||
// Priority (client): host > serverName > address
|
||||
for k := range c.Headers {
|
||||
if strings.ToLower(k) == "host" {
|
||||
return nil, errors.New(`"headers" can't contain "host"`)
|
||||
}
|
||||
}
|
||||
config := &httpupgrade.Config{
|
||||
Path: path,
|
||||
Host: c.Host,
|
||||
Header: c.Headers,
|
||||
AcceptProxyProtocol: c.AcceptProxyProtocol,
|
||||
Ed: ed,
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
const (
|
||||
Byte = 1
|
||||
Kilobyte = 1024 * Byte
|
||||
Megabyte = 1024 * Kilobyte
|
||||
Gigabyte = 1024 * Megabyte
|
||||
Terabyte = 1024 * Gigabyte
|
||||
)
|
||||
|
||||
type Bandwidth string
|
||||
|
||||
func (b Bandwidth) Bps() (uint64, error) {
|
||||
s := strings.TrimSpace(strings.ToLower(string(b)))
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
idx := len(s)
|
||||
for i, c := range s {
|
||||
if (c < '0' || c > '9') && c != '.' {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
numStr := s[:idx]
|
||||
unit := strings.TrimSpace(s[idx:])
|
||||
|
||||
val, err := strconv.ParseFloat(numStr, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
mul := uint64(1)
|
||||
switch unit {
|
||||
case "", "b", "bps":
|
||||
mul = Byte
|
||||
case "k", "kb", "kbps":
|
||||
mul = Kilobyte
|
||||
case "m", "mb", "mbps":
|
||||
mul = Megabyte
|
||||
case "g", "gb", "gbps":
|
||||
mul = Gigabyte
|
||||
case "t", "tb", "tbps":
|
||||
mul = Terabyte
|
||||
default:
|
||||
return 0, errors.New("unsupported unit: " + unit)
|
||||
}
|
||||
|
||||
return uint64(val*float64(mul)) / 8, nil
|
||||
}
|
||||
|
||||
type UdpHop struct {
|
||||
PortList PortList `json:"ports"`
|
||||
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"`
|
||||
|
||||
Congestion *string `json:"congestion"`
|
||||
Up *Bandwidth `json:"up"`
|
||||
Down *Bandwidth `json:"down"`
|
||||
UdpHop *UdpHop `json:"udphop"`
|
||||
|
||||
UdpIdleTimeout int64 `json:"udpIdleTimeout"`
|
||||
Masquerade Masquerade `json:"masquerade"`
|
||||
}
|
||||
|
||||
func (c *HysteriaConfig) Build() (proto.Message, error) {
|
||||
if c.Version != 2 {
|
||||
return nil, errors.New("version != 2")
|
||||
}
|
||||
|
||||
if c.Congestion != nil || c.Up != nil || c.Down != nil || c.UdpHop != nil {
|
||||
errors.LogWarning(context.Background(), "congestion & up & down & udphop move to finalmask/quicParams")
|
||||
}
|
||||
|
||||
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.Auth = c.Auth
|
||||
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.UdpIdleTimeout == 0 {
|
||||
config.UdpIdleTimeout = 60
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func readFileOrString(f string, s []string) ([]byte, error) {
|
||||
if len(f) > 0 {
|
||||
return filesystem.ReadCert(f)
|
||||
}
|
||||
if len(s) > 0 {
|
||||
return []byte(strings.Join(s, "\n")), nil
|
||||
}
|
||||
return nil, errors.New("both file and bytes are empty.")
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/reality"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type LimitFallback struct {
|
||||
AfterBytes uint64
|
||||
BytesPerSec uint64
|
||||
BurstBytesPerSec uint64
|
||||
}
|
||||
|
||||
type REALITYConfig struct {
|
||||
MasterKeyLog string `json:"masterKeyLog"`
|
||||
Show bool `json:"show"`
|
||||
Target json.RawMessage `json:"target"`
|
||||
Dest json.RawMessage `json:"dest"`
|
||||
Type string `json:"type"`
|
||||
Xver uint64 `json:"xver"`
|
||||
ServerNames []string `json:"serverNames"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
MinClientVer string `json:"minClientVer"`
|
||||
MaxClientVer string `json:"maxClientVer"`
|
||||
MaxTimeDiff uint64 `json:"maxTimeDiff"`
|
||||
ShortIds []string `json:"shortIds"`
|
||||
Mldsa65Seed string `json:"mldsa65Seed"`
|
||||
|
||||
LimitFallbackUpload LimitFallback `json:"limitFallbackUpload"`
|
||||
LimitFallbackDownload LimitFallback `json:"limitFallbackDownload"`
|
||||
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
ServerName string `json:"serverName"`
|
||||
Password string `json:"password"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
ShortId string `json:"shortId"`
|
||||
Mldsa65Verify string `json:"mldsa65Verify"`
|
||||
SpiderX string `json:"spiderX"`
|
||||
}
|
||||
|
||||
func (c *REALITYConfig) Build() (proto.Message, error) {
|
||||
config := new(reality.Config)
|
||||
config.MasterKeyLog = c.MasterKeyLog
|
||||
config.Show = c.Show
|
||||
var err error
|
||||
if c.Target != nil {
|
||||
c.Dest = c.Target
|
||||
}
|
||||
if c.Dest != nil {
|
||||
var i uint16
|
||||
var s string
|
||||
if err = json.Unmarshal(c.Dest, &i); err == nil {
|
||||
s = strconv.Itoa(int(i))
|
||||
} else {
|
||||
_ = json.Unmarshal(c.Dest, &s)
|
||||
}
|
||||
if c.Type == "" && s != "" {
|
||||
switch s[0] {
|
||||
case '@', '/':
|
||||
c.Type = "unix"
|
||||
if s[0] == '@' && len(s) > 1 && s[1] == '@' && (runtime.GOOS == "linux" || runtime.GOOS == "android") {
|
||||
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path)) // may need padding to work with haproxy
|
||||
copy(fullAddr, s[1:])
|
||||
s = string(fullAddr)
|
||||
}
|
||||
default:
|
||||
if _, err = strconv.Atoi(s); err == nil {
|
||||
s = "localhost:" + s
|
||||
}
|
||||
if _, _, err = net.SplitHostPort(s); err == nil {
|
||||
c.Type = "tcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.Type == "" {
|
||||
return nil, errors.New(`please fill in a valid value for "target"`)
|
||||
}
|
||||
if c.Xver > 2 {
|
||||
return nil, errors.New(`invalid PROXY protocol version, "xver" only accepts 0, 1, 2`)
|
||||
}
|
||||
if len(c.ServerNames) == 0 {
|
||||
return nil, errors.New(`empty "serverNames"`)
|
||||
}
|
||||
if c.PrivateKey == "" {
|
||||
return nil, errors.New(`empty "privateKey"`)
|
||||
}
|
||||
if config.PrivateKey, err = base64.RawURLEncoding.DecodeString(c.PrivateKey); err != nil || len(config.PrivateKey) != 32 {
|
||||
return nil, errors.New(`invalid "privateKey": `, c.PrivateKey)
|
||||
}
|
||||
if c.MinClientVer != "" {
|
||||
config.MinClientVer = make([]byte, 3)
|
||||
var u uint64
|
||||
for i, s := range strings.Split(c.MinClientVer, ".") {
|
||||
if i == 3 {
|
||||
return nil, errors.New(`invalid "minClientVer": `, c.MinClientVer)
|
||||
}
|
||||
if u, err = strconv.ParseUint(s, 10, 8); err != nil {
|
||||
return nil, errors.New(`"minClientVer[`, i, `]" should be less than 256`)
|
||||
} else {
|
||||
config.MinClientVer[i] = byte(u)
|
||||
}
|
||||
}
|
||||
errors.LogWarning(context.Background(), `REALITY: Changing "minClientVer" will increase the likelihood of your server's IP being blocked by the GFW`)
|
||||
} else {
|
||||
config.MinClientVer = []byte{26, 3, 27} // change it at your own risk: https://github.com/XTLS/Xray-core/commit/af7eb68028732a8ee3c0e5d6ab2b8a657bb2e770
|
||||
errors.LogWarning(context.Background(), `REALITY: The default minimal client version is Xray-core v26.3.27, other clients may be refused to connect`)
|
||||
}
|
||||
if c.MaxClientVer != "" {
|
||||
config.MaxClientVer = make([]byte, 3)
|
||||
var u uint64
|
||||
for i, s := range strings.Split(c.MaxClientVer, ".") {
|
||||
if i == 3 {
|
||||
return nil, errors.New(`invalid "maxClientVer": `, c.MaxClientVer)
|
||||
}
|
||||
if u, err = strconv.ParseUint(s, 10, 8); err != nil {
|
||||
return nil, errors.New(`"maxClientVer[`, i, `]" should be less than 256`)
|
||||
} else {
|
||||
config.MaxClientVer[i] = byte(u)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(c.ShortIds) == 0 {
|
||||
return nil, errors.New(`empty "shortIds"`)
|
||||
}
|
||||
config.ShortIds = make([][]byte, len(c.ShortIds))
|
||||
for i, s := range c.ShortIds {
|
||||
if len(s) > 16 {
|
||||
return nil, errors.New(`too long "shortIds[`, i, `]": `, s)
|
||||
}
|
||||
config.ShortIds[i] = make([]byte, 8)
|
||||
if _, err = hex.Decode(config.ShortIds[i], []byte(s)); err != nil {
|
||||
return nil, errors.New(`invalid "shortIds[`, i, `]": `, s)
|
||||
}
|
||||
}
|
||||
config.Dest = s
|
||||
config.Type = c.Type
|
||||
config.Xver = c.Xver
|
||||
config.ServerNames = c.ServerNames
|
||||
config.MaxTimeDiff = c.MaxTimeDiff
|
||||
|
||||
if c.Mldsa65Seed != "" {
|
||||
if c.Mldsa65Seed == c.PrivateKey {
|
||||
return nil, errors.New(`"mldsa65Seed" and "privateKey" can not be the same value: `, c.Mldsa65Seed)
|
||||
}
|
||||
if config.Mldsa65Seed, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Seed); err != nil || len(config.Mldsa65Seed) != 32 {
|
||||
return nil, errors.New(`invalid "mldsa65Seed": `, c.Mldsa65Seed)
|
||||
}
|
||||
}
|
||||
|
||||
for _, sn := range config.ServerNames {
|
||||
sn = strings.ToLower(sn)
|
||||
if strings.HasSuffix(sn, ".ru") || strings.HasSuffix(sn, ".ir") || strings.HasSuffix(sn, ".cn") ||
|
||||
strings.Contains(sn, "apple") || strings.Contains(sn, "icloud") || strings.Contains(sn, "microsoft") {
|
||||
errors.LogWarning(context.Background(), `REALITY: Choosing "`, sn, `" as the target will increase the likelihood of your server's IP being blocked by the GFW`)
|
||||
}
|
||||
}
|
||||
|
||||
config.LimitFallbackUpload = new(reality.LimitFallback)
|
||||
config.LimitFallbackUpload.AfterBytes = c.LimitFallbackUpload.AfterBytes
|
||||
config.LimitFallbackUpload.BytesPerSec = c.LimitFallbackUpload.BytesPerSec
|
||||
config.LimitFallbackUpload.BurstBytesPerSec = c.LimitFallbackUpload.BurstBytesPerSec
|
||||
config.LimitFallbackDownload = new(reality.LimitFallback)
|
||||
config.LimitFallbackDownload.AfterBytes = c.LimitFallbackDownload.AfterBytes
|
||||
config.LimitFallbackDownload.BytesPerSec = c.LimitFallbackDownload.BytesPerSec
|
||||
config.LimitFallbackDownload.BurstBytesPerSec = c.LimitFallbackDownload.BurstBytesPerSec
|
||||
} else {
|
||||
config.Fingerprint = strings.ToLower(c.Fingerprint)
|
||||
if config.Fingerprint == "unsafe" || config.Fingerprint == "hellogolang" {
|
||||
return nil, errors.New(`invalid "fingerprint": `, config.Fingerprint)
|
||||
}
|
||||
if tls.GetFingerprint(config.Fingerprint) == nil {
|
||||
return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint)
|
||||
}
|
||||
if len(c.ServerNames) != 0 {
|
||||
return nil, errors.New(`non-empty "serverNames", please use "serverName" instead`)
|
||||
}
|
||||
if c.Password != "" {
|
||||
c.PublicKey = c.Password
|
||||
}
|
||||
if c.PublicKey == "" {
|
||||
return nil, errors.New(`empty "password"`)
|
||||
}
|
||||
if config.PublicKey, err = base64.RawURLEncoding.DecodeString(c.PublicKey); err != nil || len(config.PublicKey) != 32 {
|
||||
return nil, errors.New(`invalid "password": `, c.PublicKey)
|
||||
}
|
||||
if len(c.ShortIds) != 0 {
|
||||
return nil, errors.New(`non-empty "shortIds", please use "shortId" instead`)
|
||||
}
|
||||
if len(c.ShortId) > 16 {
|
||||
return nil, errors.New(`too long "shortId": `, c.ShortId)
|
||||
}
|
||||
config.ShortId = make([]byte, 8)
|
||||
if _, err = hex.Decode(config.ShortId, []byte(c.ShortId)); err != nil {
|
||||
return nil, errors.New(`invalid "shortId": `, c.ShortId)
|
||||
}
|
||||
if c.Mldsa65Verify != "" {
|
||||
if config.Mldsa65Verify, err = base64.RawURLEncoding.DecodeString(c.Mldsa65Verify); err != nil || len(config.Mldsa65Verify) != 1952 {
|
||||
return nil, errors.New(`invalid "mldsa65Verify": `, c.Mldsa65Verify)
|
||||
}
|
||||
}
|
||||
if c.SpiderX == "" {
|
||||
c.SpiderX = "/"
|
||||
}
|
||||
if c.SpiderX[0] != '/' {
|
||||
return nil, errors.New(`invalid "spiderX": `, c.SpiderX)
|
||||
}
|
||||
config.SpiderY = make([]int64, 10)
|
||||
u, _ := url.Parse(c.SpiderX)
|
||||
q := u.Query()
|
||||
parse := func(param string, index int) {
|
||||
if q.Get(param) != "" {
|
||||
s := strings.Split(q.Get(param), "-")
|
||||
if len(s) == 1 {
|
||||
config.SpiderY[index], _ = strconv.ParseInt(s[0], 10, 64)
|
||||
config.SpiderY[index+1], _ = strconv.ParseInt(s[0], 10, 64)
|
||||
} else {
|
||||
config.SpiderY[index], _ = strconv.ParseInt(s[0], 10, 64)
|
||||
config.SpiderY[index+1], _ = strconv.ParseInt(s[1], 10, 64)
|
||||
}
|
||||
}
|
||||
q.Del(param)
|
||||
}
|
||||
parse("p", 0) // padding
|
||||
parse("c", 2) // concurrency
|
||||
parse("t", 4) // times
|
||||
parse("i", 6) // interval
|
||||
parse("r", 8) // return
|
||||
u.RawQuery = q.Encode()
|
||||
config.SpiderX = u.String()
|
||||
config.ServerName = c.ServerName
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
type TLSCertConfig struct {
|
||||
CertFile string `json:"certificateFile"`
|
||||
CertStr []string `json:"certificate"`
|
||||
KeyFile string `json:"keyFile"`
|
||||
KeyStr []string `json:"key"`
|
||||
Usage string `json:"usage"`
|
||||
OcspStapling uint64 `json:"ocspStapling"`
|
||||
OneTimeLoading bool `json:"oneTimeLoading"`
|
||||
BuildChain bool `json:"buildChain"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *TLSCertConfig) Build() (*tls.Certificate, error) {
|
||||
certificate := new(tls.Certificate)
|
||||
|
||||
cert, err := readFileOrString(c.CertFile, c.CertStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse certificate").Base(err)
|
||||
}
|
||||
certificate.Certificate = cert
|
||||
certificate.CertificatePath = c.CertFile
|
||||
|
||||
if len(c.KeyFile) > 0 || len(c.KeyStr) > 0 {
|
||||
key, err := readFileOrString(c.KeyFile, c.KeyStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse key").Base(err)
|
||||
}
|
||||
certificate.Key = key
|
||||
certificate.KeyPath = c.KeyFile
|
||||
}
|
||||
|
||||
switch strings.ToLower(c.Usage) {
|
||||
case "encipherment":
|
||||
certificate.Usage = tls.Certificate_ENCIPHERMENT
|
||||
case "verify":
|
||||
certificate.Usage = tls.Certificate_AUTHORITY_VERIFY
|
||||
case "issue":
|
||||
certificate.Usage = tls.Certificate_AUTHORITY_ISSUE
|
||||
default:
|
||||
certificate.Usage = tls.Certificate_ENCIPHERMENT
|
||||
}
|
||||
if certificate.KeyPath == "" && certificate.CertificatePath == "" {
|
||||
certificate.OneTimeLoading = true
|
||||
} else {
|
||||
certificate.OneTimeLoading = c.OneTimeLoading
|
||||
}
|
||||
certificate.OcspStapling = c.OcspStapling
|
||||
certificate.BuildChain = c.BuildChain
|
||||
|
||||
return certificate, nil
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
AllowInsecure bool `json:"allowInsecure"`
|
||||
Certs []*TLSCertConfig `json:"certificates"`
|
||||
ServerName string `json:"serverName"`
|
||||
ALPN *StringList `json:"alpn"`
|
||||
EnableSessionResumption bool `json:"enableSessionResumption"`
|
||||
DisableSystemRoot bool `json:"disableSystemRoot"`
|
||||
MinVersion string `json:"minVersion"`
|
||||
MaxVersion string `json:"maxVersion"`
|
||||
CipherSuites string `json:"cipherSuites"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
RejectUnknownSNI bool `json:"rejectUnknownSni"`
|
||||
CurvePreferences *StringList `json:"curvePreferences"`
|
||||
MasterKeyLog string `json:"masterKeyLog"`
|
||||
PinnedPeerCertSha256 string `json:"pinnedPeerCertSha256"`
|
||||
VerifyPeerCertByName string `json:"verifyPeerCertByName"`
|
||||
ECHServerKeys string `json:"echServerKeys"`
|
||||
ECHConfigList string `json:"echConfigList"`
|
||||
ECHSocketSettings *SocketConfig `json:"echSockopt"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *TLSConfig) Build() (proto.Message, error) {
|
||||
config := new(tls.Config)
|
||||
config.Certificate = make([]*tls.Certificate, len(c.Certs))
|
||||
for idx, certConf := range c.Certs {
|
||||
cert, err := certConf.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Certificate[idx] = cert
|
||||
}
|
||||
serverName := c.ServerName
|
||||
if len(c.ServerName) > 0 {
|
||||
config.ServerName = serverName
|
||||
}
|
||||
if c.ALPN != nil && len(*c.ALPN) > 0 {
|
||||
config.NextProtocol = []string(*c.ALPN)
|
||||
}
|
||||
if len(config.NextProtocol) > 1 {
|
||||
for _, p := range config.NextProtocol {
|
||||
if tls.IsFromMitm(p) {
|
||||
return nil, errors.New(`only one element is allowed in "alpn" when using "fromMitm" in it`)
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.CurvePreferences != nil && len(*c.CurvePreferences) > 0 {
|
||||
config.CurvePreferences = []string(*c.CurvePreferences)
|
||||
}
|
||||
config.EnableSessionResumption = c.EnableSessionResumption
|
||||
config.DisableSystemRoot = c.DisableSystemRoot
|
||||
config.MinVersion = c.MinVersion
|
||||
config.MaxVersion = c.MaxVersion
|
||||
config.CipherSuites = c.CipherSuites
|
||||
config.Fingerprint = strings.ToLower(c.Fingerprint)
|
||||
if config.Fingerprint != "unsafe" && tls.GetFingerprint(config.Fingerprint) == nil {
|
||||
return nil, errors.New(`unknown "fingerprint": `, config.Fingerprint)
|
||||
}
|
||||
config.RejectUnknownSni = c.RejectUnknownSNI
|
||||
config.MasterKeyLog = c.MasterKeyLog
|
||||
|
||||
if c.AllowInsecure {
|
||||
return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"(pcs) and "verifyPeerCertByName"(vcn)`)
|
||||
}
|
||||
if c.PinnedPeerCertSha256 != "" {
|
||||
for v := range strings.SplitSeq(c.PinnedPeerCertSha256, ",") {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
// remove colons for OpenSSL format
|
||||
hashValue, err := hex.DecodeString(strings.ReplaceAll(v, ":", ""))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(hashValue) != 32 {
|
||||
return nil, errors.New("incorrect pinnedPeerCertSha256 length: ", v)
|
||||
}
|
||||
config.PinnedPeerCertSha256 = append(config.PinnedPeerCertSha256, hashValue)
|
||||
}
|
||||
}
|
||||
if c.VerifyPeerCertByName != "" {
|
||||
for v := range strings.SplitSeq(c.VerifyPeerCertByName, ",") {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
config.VerifyPeerCertByName = append(config.VerifyPeerCertByName, v)
|
||||
}
|
||||
}
|
||||
|
||||
if c.ECHServerKeys != "" {
|
||||
EchPrivateKey, err := base64.StdEncoding.DecodeString(c.ECHServerKeys)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid ECH Config", c.ECHServerKeys)
|
||||
}
|
||||
config.EchServerKeys = EchPrivateKey
|
||||
}
|
||||
config.EchConfigList = c.ECHConfigList
|
||||
if c.ECHSocketSettings != nil {
|
||||
ss, err := c.ECHSocketSettings.Build()
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to build ech sockopt.").Base(err)
|
||||
}
|
||||
config.EchSocketSettings = ss
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
type CustomSockoptConfig struct {
|
||||
Syetem string `json:"system"`
|
||||
Network string `json:"network"`
|
||||
Level string `json:"level"`
|
||||
Opt string `json:"opt"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type HappyEyeballsConfig struct {
|
||||
PrioritizeIPv6 bool `json:"prioritizeIPv6"`
|
||||
TryDelayMs uint64 `json:"tryDelayMs"`
|
||||
Interleave uint32 `json:"interleave"`
|
||||
MaxConcurrentTry uint32 `json:"maxConcurrentTry"`
|
||||
}
|
||||
|
||||
func (h *HappyEyeballsConfig) UnmarshalJSON(data []byte) error {
|
||||
innerHappyEyeballsConfig := struct {
|
||||
PrioritizeIPv6 bool `json:"prioritizeIPv6"`
|
||||
TryDelayMs uint64 `json:"tryDelayMs"`
|
||||
Interleave uint32 `json:"interleave"`
|
||||
MaxConcurrentTry uint32 `json:"maxConcurrentTry"`
|
||||
}{PrioritizeIPv6: false, Interleave: 1, TryDelayMs: 0, MaxConcurrentTry: 4}
|
||||
if err := json.Unmarshal(data, &innerHappyEyeballsConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
h.PrioritizeIPv6 = innerHappyEyeballsConfig.PrioritizeIPv6
|
||||
h.TryDelayMs = innerHappyEyeballsConfig.TryDelayMs
|
||||
h.Interleave = innerHappyEyeballsConfig.Interleave
|
||||
h.MaxConcurrentTry = innerHappyEyeballsConfig.MaxConcurrentTry
|
||||
return nil
|
||||
}
|
||||
|
||||
type SocketConfig struct {
|
||||
Mark int32 `json:"mark"`
|
||||
TFO interface{} `json:"tcpFastOpen"`
|
||||
TProxy string `json:"tproxy"`
|
||||
AcceptProxyProtocol bool `json:"acceptProxyProtocol"`
|
||||
DomainStrategy string `json:"domainStrategy"`
|
||||
DialerProxy string `json:"dialerProxy"`
|
||||
TCPKeepAliveInterval int32 `json:"tcpKeepAliveInterval"`
|
||||
TCPKeepAliveIdle int32 `json:"tcpKeepAliveIdle"`
|
||||
TCPCongestion string `json:"tcpCongestion"`
|
||||
TCPWindowClamp int32 `json:"tcpWindowClamp"`
|
||||
TCPMaxSeg int32 `json:"tcpMaxSeg"`
|
||||
Penetrate bool `json:"penetrate"`
|
||||
TCPUserTimeout int32 `json:"tcpUserTimeout"`
|
||||
V6only bool `json:"v6only"`
|
||||
Interface string `json:"interface"`
|
||||
TcpMptcp bool `json:"tcpMptcp"`
|
||||
CustomSockopt []*CustomSockoptConfig `json:"customSockopt"`
|
||||
AddressPortStrategy string `json:"addressPortStrategy"`
|
||||
HappyEyeballsSettings *HappyEyeballsConfig `json:"happyEyeballs"`
|
||||
TrustedXForwardedFor []string `json:"trustedXForwardedFor"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *SocketConfig) Build() (*internet.SocketConfig, error) {
|
||||
tfo := int32(0) // don't invoke setsockopt() for TFO
|
||||
if c.TFO != nil {
|
||||
switch v := c.TFO.(type) {
|
||||
case bool:
|
||||
if v {
|
||||
tfo = 256
|
||||
} else {
|
||||
tfo = -1 // TFO need to be disabled
|
||||
}
|
||||
case float64:
|
||||
tfo = int32(math.Min(v, math.MaxInt32))
|
||||
default:
|
||||
return nil, errors.New("tcpFastOpen: only boolean and integer value is acceptable")
|
||||
}
|
||||
}
|
||||
var tproxy internet.SocketConfig_TProxyMode
|
||||
switch strings.ToLower(c.TProxy) {
|
||||
case "tproxy":
|
||||
tproxy = internet.SocketConfig_TProxy
|
||||
case "redirect":
|
||||
tproxy = internet.SocketConfig_Redirect
|
||||
default:
|
||||
tproxy = internet.SocketConfig_Off
|
||||
}
|
||||
|
||||
dStrategy := internet.DomainStrategy_AS_IS
|
||||
switch strings.ToLower(c.DomainStrategy) {
|
||||
case "asis", "":
|
||||
dStrategy = internet.DomainStrategy_AS_IS
|
||||
case "useip":
|
||||
dStrategy = internet.DomainStrategy_USE_IP
|
||||
case "useipv4":
|
||||
dStrategy = internet.DomainStrategy_USE_IP4
|
||||
case "useipv6":
|
||||
dStrategy = internet.DomainStrategy_USE_IP6
|
||||
case "useipv4v6":
|
||||
dStrategy = internet.DomainStrategy_USE_IP46
|
||||
case "useipv6v4":
|
||||
dStrategy = internet.DomainStrategy_USE_IP64
|
||||
case "forceip":
|
||||
dStrategy = internet.DomainStrategy_FORCE_IP
|
||||
case "forceipv4":
|
||||
dStrategy = internet.DomainStrategy_FORCE_IP4
|
||||
case "forceipv6":
|
||||
dStrategy = internet.DomainStrategy_FORCE_IP6
|
||||
case "forceipv4v6":
|
||||
dStrategy = internet.DomainStrategy_FORCE_IP46
|
||||
case "forceipv6v4":
|
||||
dStrategy = internet.DomainStrategy_FORCE_IP64
|
||||
default:
|
||||
return nil, errors.New("unsupported domain strategy: ", c.DomainStrategy)
|
||||
}
|
||||
|
||||
var customSockopts []*internet.CustomSockopt
|
||||
|
||||
for _, copt := range c.CustomSockopt {
|
||||
customSockopt := &internet.CustomSockopt{
|
||||
System: copt.Syetem,
|
||||
Network: copt.Network,
|
||||
Level: copt.Level,
|
||||
Opt: copt.Opt,
|
||||
Value: copt.Value,
|
||||
Type: copt.Type,
|
||||
}
|
||||
customSockopts = append(customSockopts, customSockopt)
|
||||
}
|
||||
|
||||
addressPortStrategy := internet.AddressPortStrategy_None
|
||||
switch strings.ToLower(c.AddressPortStrategy) {
|
||||
case "none", "":
|
||||
addressPortStrategy = internet.AddressPortStrategy_None
|
||||
case "srvportonly":
|
||||
addressPortStrategy = internet.AddressPortStrategy_SrvPortOnly
|
||||
case "srvaddressonly":
|
||||
addressPortStrategy = internet.AddressPortStrategy_SrvAddressOnly
|
||||
case "srvportandaddress":
|
||||
addressPortStrategy = internet.AddressPortStrategy_SrvPortAndAddress
|
||||
case "txtportonly":
|
||||
addressPortStrategy = internet.AddressPortStrategy_TxtPortOnly
|
||||
case "txtaddressonly":
|
||||
addressPortStrategy = internet.AddressPortStrategy_TxtAddressOnly
|
||||
case "txtportandaddress":
|
||||
addressPortStrategy = internet.AddressPortStrategy_TxtPortAndAddress
|
||||
default:
|
||||
return nil, errors.New("unsupported address and port strategy: ", c.AddressPortStrategy)
|
||||
}
|
||||
|
||||
happyEyeballs := &internet.HappyEyeballsConfig{Interleave: 1, PrioritizeIpv6: false, TryDelayMs: 0, MaxConcurrentTry: 4}
|
||||
if c.HappyEyeballsSettings != nil {
|
||||
happyEyeballs.PrioritizeIpv6 = c.HappyEyeballsSettings.PrioritizeIPv6
|
||||
happyEyeballs.Interleave = c.HappyEyeballsSettings.Interleave
|
||||
happyEyeballs.TryDelayMs = c.HappyEyeballsSettings.TryDelayMs
|
||||
happyEyeballs.MaxConcurrentTry = c.HappyEyeballsSettings.MaxConcurrentTry
|
||||
}
|
||||
|
||||
return &internet.SocketConfig{
|
||||
Mark: c.Mark,
|
||||
Tfo: tfo,
|
||||
Tproxy: tproxy,
|
||||
DomainStrategy: dStrategy,
|
||||
AcceptProxyProtocol: c.AcceptProxyProtocol,
|
||||
DialerProxy: c.DialerProxy,
|
||||
TcpKeepAliveInterval: c.TCPKeepAliveInterval,
|
||||
TcpKeepAliveIdle: c.TCPKeepAliveIdle,
|
||||
TcpCongestion: c.TCPCongestion,
|
||||
TcpWindowClamp: c.TCPWindowClamp,
|
||||
TcpMaxSeg: c.TCPMaxSeg,
|
||||
Penetrate: c.Penetrate,
|
||||
TcpUserTimeout: c.TCPUserTimeout,
|
||||
V6Only: c.V6only,
|
||||
Interface: c.Interface,
|
||||
TcpMptcp: c.TcpMptcp,
|
||||
CustomSockopt: customSockopts,
|
||||
AddressPortStrategy: addressPortStrategy,
|
||||
HappyEyeballs: happyEyeballs,
|
||||
TrustedXForwardedFor: c.TrustedXForwardedFor,
|
||||
}, nil
|
||||
}
|
||||
+70
-1
@@ -1,12 +1,19 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/xtls/xray-core/proxy/tun"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type TunConfig struct {
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
MTU uint32 `json:"mtu"`
|
||||
Gateway []string `json:"gateway"`
|
||||
DNS []string `json:"dns"`
|
||||
@@ -18,6 +25,7 @@ type TunConfig struct {
|
||||
func (v *TunConfig) Build() (proto.Message, error) {
|
||||
config := &tun.Config{
|
||||
Name: v.Name,
|
||||
Desc: v.Desc,
|
||||
MTU: v.MTU,
|
||||
Gateway: v.Gateway,
|
||||
DNS: v.DNS,
|
||||
@@ -32,10 +40,71 @@ func (v *TunConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
if config.Name == "" {
|
||||
config.Name = "xray0"
|
||||
name, err := GetAvailableTunName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Name = name
|
||||
}
|
||||
if config.Desc == "" {
|
||||
config.Desc = "Wintun"
|
||||
}
|
||||
if config.MTU == 0 {
|
||||
config.MTU = 1500
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
const (
|
||||
tunNamePrefix = "utun"
|
||||
minTunIndex = 10
|
||||
maxTunIndex = 1024
|
||||
)
|
||||
|
||||
func GetAvailableTunName() (string, error) {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fail to get system interface information: %w", err)
|
||||
}
|
||||
|
||||
usedNames := make(map[string]struct{}, len(interfaces))
|
||||
for _, iface := range interfaces {
|
||||
usedNames[iface.Name] = struct{}{}
|
||||
}
|
||||
|
||||
startIndex, err := randomInt(minTunIndex, maxTunIndex)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fail to generate valid tun name: %w", err)
|
||||
}
|
||||
|
||||
rangeSize := maxTunIndex - minTunIndex + 1
|
||||
|
||||
for offset := 0; offset < rangeSize; offset++ {
|
||||
index := minTunIndex + (startIndex-minTunIndex+offset)%rangeSize
|
||||
name := tunNamePrefix + strconv.Itoa(index)
|
||||
|
||||
if _, exists := usedNames[name]; !exists {
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf(
|
||||
"no available TUN interface name in range %s%d-%s%d",
|
||||
tunNamePrefix,
|
||||
minTunIndex,
|
||||
tunNamePrefix,
|
||||
maxTunIndex,
|
||||
)
|
||||
}
|
||||
|
||||
func randomInt(min, max int) (int, error) {
|
||||
value, err := rand.Int(
|
||||
rand.Reader,
|
||||
big.NewInt(int64(max-min+1)),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return min + int(value.Int64()), nil
|
||||
}
|
||||
|
||||
@@ -31,10 +31,6 @@ func (a *VMessAccount) Build() *vmess.Account {
|
||||
st = protocol.SecurityType_CHACHA20_POLY1305
|
||||
case "auto":
|
||||
st = protocol.SecurityType_AUTO
|
||||
case "none":
|
||||
st = protocol.SecurityType_NONE
|
||||
case "zero":
|
||||
st = protocol.SecurityType_ZERO
|
||||
default:
|
||||
st = protocol.SecurityType_AUTO
|
||||
}
|
||||
|
||||
+60
-1
@@ -3,6 +3,7 @@ package conf
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -175,7 +176,7 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
|
||||
receiverSettings.StreamSettings = ss
|
||||
if strings.Contains(ss.SecurityType, "reality") && (receiverSettings.PortList == nil ||
|
||||
len(receiverSettings.PortList.Ports()) != 1 || receiverSettings.PortList.Ports()[0] != 443) {
|
||||
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports may get your IP blocked by the GFW`)
|
||||
errors.LogWarning(context.Background(), `REALITY: Listening on non-443 ports will increase the likelihood of your server's IP being blocked by the GFW`)
|
||||
}
|
||||
}
|
||||
if c.SniffingConfig != nil {
|
||||
@@ -230,6 +231,40 @@ func (c *OutboundDetourConfig) checkChainProxyConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func requiresTransportSecurity(address *Address) bool {
|
||||
if address == nil || address.Address == nil {
|
||||
return false
|
||||
}
|
||||
if address.Family().IsIP() {
|
||||
return !geodata.GetPrivateIPMatcher().Match(address.IP())
|
||||
}
|
||||
domain := strings.TrimSuffix(strings.ToLower(address.Domain()), ".")
|
||||
return !geodata.GetPrivateDomainMatcher().MatchAny(domain)
|
||||
}
|
||||
|
||||
func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *proxyman.SenderConfig) error {
|
||||
if senderSettings.StreamSettings != nil && senderSettings.StreamSettings.GetSecurityType() != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if vlessCfg, ok := rawConfig.(*VLessOutboundConfig); ok {
|
||||
if vlessCfg.Encryption != "" && vlessCfg.Encryption != "none" {
|
||||
return nil
|
||||
}
|
||||
if requiresTransportSecurity(vlessCfg.Address) {
|
||||
return errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain")
|
||||
}
|
||||
}
|
||||
|
||||
if tjCfg, ok := rawConfig.(*TrojanClientConfig); ok {
|
||||
if requiresTransportSecurity(tjCfg.Address) {
|
||||
return errors.New("trojan without TLS is prohibited unless the server address is a private IP or domain")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
senderSettings := &proxyman.SenderConfig{}
|
||||
@@ -323,6 +358,9 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to load outbound detour config for protocol ", c.Protocol).Base(err)
|
||||
}
|
||||
if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts, err := rawConfig.(Buildable).Build()
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err)
|
||||
@@ -342,11 +380,20 @@ func (c *StatsConfig) Build() (*stats.Config, error) {
|
||||
return &stats.Config{}, nil
|
||||
}
|
||||
|
||||
type EnvConfig map[string]string
|
||||
|
||||
func (c EnvConfig) Override(o EnvConfig) {
|
||||
for key, value := range o {
|
||||
c[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// Deprecated: Global transport config is no longer used
|
||||
// left for returning error
|
||||
Transport map[string]json.RawMessage `json:"transport"`
|
||||
|
||||
Env EnvConfig `json:"env"`
|
||||
LogConfig *LogConfig `json:"log"`
|
||||
RouterConfig *RouterConfig `json:"routing"`
|
||||
DNSConfig *DNSConfig `json:"dns"`
|
||||
@@ -402,6 +449,12 @@ func (c *Config) Override(o *Config, fn string) {
|
||||
if o.Transport != nil {
|
||||
c.Transport = o.Transport
|
||||
}
|
||||
if o.Env != nil {
|
||||
if c.Env == nil {
|
||||
c.Env = EnvConfig{}
|
||||
}
|
||||
c.Env.Override(o.Env)
|
||||
}
|
||||
if o.Policy != nil {
|
||||
c.Policy = o.Policy
|
||||
}
|
||||
@@ -477,6 +530,12 @@ func (c *Config) Override(o *Config, fn string) {
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *Config) Build() (*core.Config, error) {
|
||||
for key, value := range c.Env {
|
||||
if err := os.Setenv(key, value); err != nil {
|
||||
return nil, errors.New("failed to apply environment configuration").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := PostProcessConfigureFile(c); err != nil {
|
||||
return nil, errors.New("failed to post-process configuration file").Base(err)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ The -confdir=dir flag sets a dir with multiple json config
|
||||
The -format=json flag sets the format of config files.
|
||||
Default "auto".
|
||||
|
||||
The config root env object sets process environment variables after all config
|
||||
files are parsed. Variables needed to locate or parse config files must be set
|
||||
in the process environment before Xray starts.
|
||||
|
||||
The -test flag tells Xray to test config files only,
|
||||
without launching the server.
|
||||
|
||||
|
||||
+17
-28
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pires/go-proxyproto"
|
||||
@@ -31,12 +32,24 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
useSplice bool
|
||||
useSplice atomic.Bool
|
||||
allNetworks [8]bool
|
||||
defaultBlockPrivateRule *FinalRule
|
||||
defaultBlockAllRule *FinalRule
|
||||
)
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
|
||||
enabled := false
|
||||
switch value {
|
||||
case defaultFlagValue, "auto", "enable":
|
||||
enabled = true
|
||||
}
|
||||
useSplice.Store(enabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
h := new(Handler)
|
||||
@@ -48,12 +61,7 @@ func init() {
|
||||
return h, nil
|
||||
}))
|
||||
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
value := platform.NewEnvFlag(platform.UseFreedomSplice).GetValue(func() string { return defaultFlagValue })
|
||||
switch value {
|
||||
case defaultFlagValue, "auto", "enable":
|
||||
useSplice = true
|
||||
}
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
|
||||
for i := range allNetworks {
|
||||
allNetworks[i] = true
|
||||
@@ -62,26 +70,7 @@ func init() {
|
||||
defaultBlockPrivateRule = &FinalRule{
|
||||
action: RuleAction_Block,
|
||||
network: allNetworks,
|
||||
ip: common.Must2(geodata.IPReg.BuildIPMatcher(common.Must2(geodata.ParseIPRules([]string{
|
||||
"0.0.0.0/8",
|
||||
"10.0.0.0/8",
|
||||
"100.64.0.0/10",
|
||||
"127.0.0.0/8",
|
||||
"169.254.0.0/16",
|
||||
"172.16.0.0/12",
|
||||
"192.0.0.0/24",
|
||||
"192.0.2.0/24",
|
||||
"192.88.99.0/24",
|
||||
"192.168.0.0/16",
|
||||
"198.18.0.0/15",
|
||||
"198.51.100.0/24",
|
||||
"203.0.113.0/24",
|
||||
"224.0.0.0/3",
|
||||
"::/127",
|
||||
"fc00::/7",
|
||||
"fe80::/10",
|
||||
"ff00::/8",
|
||||
})))),
|
||||
ip: geodata.GetPrivateIPMatcher(),
|
||||
}
|
||||
|
||||
defaultBlockAllRule = &FinalRule{
|
||||
@@ -441,7 +430,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(plcy.Timeouts.UplinkOnly)
|
||||
if destination.Network == net.Network_TCP && useSplice && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic
|
||||
if destination.Network == net.Network_TCP && useSplice.Load() && proxy.IsRAWTransportWithoutSecurity(conn) { // it would be tls conn in special use case of MITM, we need to let link handle traffic
|
||||
var writeConn net.Conn
|
||||
var inTimer *signal.ActivityTimer
|
||||
if inbound := session.InboundFromContext(ctx); inbound != nil && inbound.Conn != nil {
|
||||
|
||||
@@ -5,14 +5,14 @@ import (
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/uuid"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func (a *Account) AsAccount() (protocol.Account, error) {
|
||||
var VR net.Port
|
||||
if id, err := uuid.ParseString(a.Auth); err == nil {
|
||||
if id, err := uuid.Parse(a.Auth); err == nil {
|
||||
VR = net.PortFromBytes(id[6:8])
|
||||
}
|
||||
return &MemoryAccount{
|
||||
@@ -41,29 +41,71 @@ func (a *MemoryAccount) ToProto() proto.Message {
|
||||
|
||||
type Validator struct {
|
||||
users sync.Map
|
||||
ids sync.Map
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewValidator() *Validator {
|
||||
return &Validator{}
|
||||
}
|
||||
|
||||
func (v *Validator) Add(user *protocol.MemoryUser) error {
|
||||
func (v *Validator) Add(user *protocol.MemoryUser) (err error) {
|
||||
v.mu.Lock()
|
||||
v.users.Store(user.Account.(*MemoryAccount).Auth, user)
|
||||
return nil
|
||||
if id, err := uuid.Parse(user.Account.(*MemoryAccount).Auth); err == nil {
|
||||
id[6] = 0
|
||||
id[7] = 0
|
||||
v.ids.Store(id, user)
|
||||
}
|
||||
v.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (v *Validator) DelByEmail(email string) error {
|
||||
func (v *Validator) DelByEmail(email string) (err error) {
|
||||
v.mu.Lock()
|
||||
if user := v.GetByEmail(email); user != nil {
|
||||
v.users.Delete(user.Account.(*MemoryAccount).Auth)
|
||||
auth := user.Account.(*MemoryAccount).Auth
|
||||
v.users.Delete(auth)
|
||||
if id, err := uuid.Parse(auth); err == nil {
|
||||
id[6] = 0
|
||||
id[7] = 0
|
||||
v.ids.Delete(id)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
v.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (v *Validator) Get(auth string) *protocol.MemoryUser {
|
||||
if value, ok := v.users.Load(auth); ok {
|
||||
return value.(*protocol.MemoryUser)
|
||||
func (v *Validator) Get(auth string) (user *protocol.MemoryUser) {
|
||||
if id, err := uuid.Parse(auth); err == nil {
|
||||
if user = v.GetByID(id); user != nil {
|
||||
VR := net.PortFromBytes(id[6:8])
|
||||
if user.Account.(*MemoryAccount).VR != VR {
|
||||
user = &protocol.MemoryUser{
|
||||
Email: user.Email,
|
||||
Level: user.Level,
|
||||
Account: &MemoryAccount{
|
||||
Auth: auth,
|
||||
VR: VR,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
return nil
|
||||
if value, ok := v.users.Load(auth); ok {
|
||||
user = value.(*protocol.MemoryUser)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (v *Validator) GetByID(id uuid.UUID) (user *protocol.MemoryUser) {
|
||||
id[6] = 0
|
||||
id[7] = 0
|
||||
if value, ok := v.ids.Load(id); ok {
|
||||
user = value.(*protocol.MemoryUser)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (v *Validator) GetByEmail(email string) (user *protocol.MemoryUser) {
|
||||
|
||||
@@ -85,8 +85,6 @@ func (a *Account) getCipher() (Cipher, error) {
|
||||
IVBytes: 32,
|
||||
AEADAuthCreator: createXChaCha20Poly1305,
|
||||
}, nil
|
||||
case CipherType_NONE:
|
||||
return NoneCipher{}, nil
|
||||
default:
|
||||
return nil, errors.New("Unsupported cipher.")
|
||||
}
|
||||
@@ -186,30 +184,6 @@ func (c *AEADCipher) DecodePacket(key []byte, b *buf.Buffer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type NoneCipher struct{}
|
||||
|
||||
func (NoneCipher) KeySize() int32 { return 0 }
|
||||
func (NoneCipher) IVSize() int32 { return 0 }
|
||||
func (NoneCipher) IsAEAD() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (NoneCipher) NewDecryptionReader(key []byte, iv []byte, reader io.Reader) (buf.Reader, error) {
|
||||
return buf.NewReader(reader), nil
|
||||
}
|
||||
|
||||
func (NoneCipher) NewEncryptionWriter(key []byte, iv []byte, writer io.Writer) (buf.Writer, error) {
|
||||
return buf.NewWriter(writer), nil
|
||||
}
|
||||
|
||||
func (NoneCipher) EncodePacket(key []byte, b *buf.Buffer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NoneCipher) DecodePacket(key []byte, b *buf.Buffer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func passwordToCipherKey(password []byte, keySize int32) []byte {
|
||||
key := make([]byte, 0, keySize)
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ const (
|
||||
CipherType_AES_256_GCM CipherType = 6
|
||||
CipherType_CHACHA20_POLY1305 CipherType = 7
|
||||
CipherType_XCHACHA20_POLY1305 CipherType = 8
|
||||
CipherType_NONE CipherType = 9
|
||||
)
|
||||
|
||||
// Enum value maps for CipherType.
|
||||
@@ -42,7 +41,6 @@ var (
|
||||
6: "AES_256_GCM",
|
||||
7: "CHACHA20_POLY1305",
|
||||
8: "XCHACHA20_POLY1305",
|
||||
9: "NONE",
|
||||
}
|
||||
CipherType_value = map[string]int32{
|
||||
"UNKNOWN": 0,
|
||||
@@ -50,7 +48,6 @@ var (
|
||||
"AES_256_GCM": 6,
|
||||
"CHACHA20_POLY1305": 7,
|
||||
"XCHACHA20_POLY1305": 8,
|
||||
"NONE": 9,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -251,15 +248,14 @@ const file_proxy_shadowsocks_config_proto_rawDesc = "" +
|
||||
"\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x122\n" +
|
||||
"\anetwork\x18\x02 \x03(\x0e2\x18.xray.common.net.NetworkR\anetwork\"L\n" +
|
||||
"\fClientConfig\x12<\n" +
|
||||
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server*t\n" +
|
||||
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server*j\n" +
|
||||
"\n" +
|
||||
"CipherType\x12\v\n" +
|
||||
"\aUNKNOWN\x10\x00\x12\x0f\n" +
|
||||
"\vAES_128_GCM\x10\x05\x12\x0f\n" +
|
||||
"\vAES_256_GCM\x10\x06\x12\x15\n" +
|
||||
"\x11CHACHA20_POLY1305\x10\a\x12\x16\n" +
|
||||
"\x12XCHACHA20_POLY1305\x10\b\x12\b\n" +
|
||||
"\x04NONE\x10\tBd\n" +
|
||||
"\x12XCHACHA20_POLY1305\x10\bBd\n" +
|
||||
"\x1acom.xray.proxy.shadowsocksP\x01Z+github.com/xtls/xray-core/proxy/shadowsocks\xaa\x02\x16Xray.Proxy.Shadowsocksb\x06proto3"
|
||||
|
||||
var (
|
||||
|
||||
@@ -23,7 +23,6 @@ enum CipherType {
|
||||
AES_256_GCM = 6;
|
||||
CHACHA20_POLY1305 = 7;
|
||||
XCHACHA20_POLY1305 = 8;
|
||||
NONE = 9;
|
||||
}
|
||||
|
||||
message ServerConfig {
|
||||
|
||||
@@ -38,19 +38,6 @@ func TestUDPEncodingDecoding(t *testing.T) {
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
Version: Version,
|
||||
Command: protocol.RequestCommandUDP,
|
||||
Address: net.LocalHostIP,
|
||||
Port: 1234,
|
||||
User: &protocol.MemoryUser{
|
||||
Email: "love@example.com",
|
||||
Account: toAccount(&Account{
|
||||
Password: "123",
|
||||
CipherType: CipherType_NONE,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, request := range testRequests {
|
||||
@@ -80,10 +67,6 @@ func TestUDPDecodingWithPayloadTooShort(t *testing.T) {
|
||||
Password: "password",
|
||||
CipherType: CipherType_AES_128_GCM,
|
||||
}),
|
||||
toAccount(&Account{
|
||||
Password: "password",
|
||||
CipherType: CipherType_NONE,
|
||||
}),
|
||||
}
|
||||
|
||||
for _, account := range testAccounts {
|
||||
|
||||
@@ -145,7 +145,6 @@ func (v *Validator) Get(bs []byte, command protocol.RequestCommand) (u *protocol
|
||||
} else {
|
||||
u = user
|
||||
ivLen = user.Account.(*MemoryAccount).Cipher.IVSize()
|
||||
// err = user.Account.(*MemoryAccount).CheckIV(bs[:ivLen]) // The IV size of None Cipher is 0.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
+18
-7
@@ -14,11 +14,12 @@ Plainly enabling it in the config probably will result nothing, or lock your rou
|
||||
|
||||
## DETAILS
|
||||
|
||||
Current implementation does not contain options to configure network level addresses, routing or rules.
|
||||
Enabling the feature will result only tun interface up, and that's it. \
|
||||
This is explicit decision, significantly simplifying implementation, and allowing any number of custom configurations, consumers could come up with. Network interface is OS level entity, and OS is what should manage it. \
|
||||
Working configuration, is tun enabled in Xray config with specific name (e.g. xray0), and OS level configuration to manage "xray0" interface, applying routing and rules on interface up.
|
||||
This way consistency of system level routing and rules is ensured from single place of responsibility - the OS itself. \
|
||||
By default, enabling the feature will only bring the tun interface up. \
|
||||
When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS uses the first IPv4 prefix from `gateway` to configure the utun point-to-point address. \
|
||||
Windows, Linux and macOS can also apply system routes from `autoSystemRoutingTable`.
|
||||
Linux and macOS do not configure system DNS from the `dns` field; system DNS remains managed by the OS or distribution-specific network services. \
|
||||
For more advanced routing policies or rules, OS level configuration can still manage the named interface (e.g. xray0) when it appears.
|
||||
This keeps complex system level routing and rules in a single place of responsibility - the OS itself. \
|
||||
Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README.
|
||||
|
||||
Due to this inbound not actually being a proxy, the configuration ignore required listen and port options, and never listen on any port. \
|
||||
@@ -30,13 +31,17 @@ Here is simple Xray config snippet to enable the inbound:
|
||||
"port": 0,
|
||||
"protocol": "tun",
|
||||
"settings": {
|
||||
"name": "xray0",
|
||||
"MTU": 1492
|
||||
"name": "utun10",
|
||||
"desc": "Wintun",
|
||||
"mtu": 1500
|
||||
}
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
`desc` sets the Windows Wintun adapter tunnel type and defaults to `Wintun`.
|
||||
It is ignored on other platforms.
|
||||
|
||||
## SUPPORTED FEATURES
|
||||
|
||||
- IPv4 and IPv6
|
||||
@@ -206,6 +211,11 @@ ifconfig
|
||||
Produced list will have all system interfaces listed, from which you will see how many "utun" ones already exists.
|
||||
It's not required to select next available number, e.g. if you have utun1-utun7 interfaces, it's not required to have "utun8" in the config. You can choose any available name, even utun20, to get surely available interface number.
|
||||
|
||||
macOS requires the utun interface to have a point-to-point IPv4 address before IPv4 routes can use it. \
|
||||
By default Xray uses `169.254.10.1/30` as the remote gateway address and assigns the next address in the prefix to the local utun side. \
|
||||
You can override this by setting `gateway`; macOS uses the first IPv4 prefix in the list. IPv6 `gateway` entries are not used for utun addressing, and IPv6 routes use the interface route instead. \
|
||||
The `dns` field does not change macOS system DNS.
|
||||
|
||||
To attach routing to the interface, route command like following can be executed:
|
||||
```
|
||||
sudo route add -net 1.1.1.0/24 -iface utun10
|
||||
@@ -214,6 +224,7 @@ sudo route add -net 1.1.1.0/24 -iface utun10
|
||||
sudo route add -inet6 -host 2606:4700:4700::1111 -iface utun10
|
||||
sudo route add -inet6 -host 2606:4700:4700::1001 -iface utun10
|
||||
```
|
||||
Alternatively, configure `autoSystemRoutingTable` and Xray will add and remove those system routes while it is running.
|
||||
Important to remember that everything written above about Linux routing concept, also apply to Mac OS X. If you simply route default route through utun interface, that will result network loop and immediate network failure.
|
||||
|
||||
## ANDROID SUPPORT
|
||||
|
||||
+14
-4
@@ -7,11 +7,12 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,6 +31,7 @@ type Config struct {
|
||||
UserLevel uint32 `protobuf:"varint,5,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"`
|
||||
AutoSystemRoutingTable []string `protobuf:"bytes,6,rep,name=auto_system_routing_table,json=autoSystemRoutingTable,proto3" json:"auto_system_routing_table,omitempty"`
|
||||
AutoOutboundsInterface string `protobuf:"bytes,7,opt,name=auto_outbounds_interface,json=autoOutboundsInterface,proto3" json:"auto_outbounds_interface,omitempty"`
|
||||
Desc string `protobuf:"bytes,8,opt,name=desc,proto3" json:"desc,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -113,11 +115,18 @@ func (x *Config) GetAutoOutboundsInterface() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetDesc() string {
|
||||
if x != nil {
|
||||
return x.Desc
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_proxy_tun_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xee\x01\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\x82\x02\n" +
|
||||
"\x06Config\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
||||
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
|
||||
@@ -126,7 +135,8 @@ const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"user_level\x18\x05 \x01(\rR\tuserLevel\x129\n" +
|
||||
"\x19auto_system_routing_table\x18\x06 \x03(\tR\x16autoSystemRoutingTable\x128\n" +
|
||||
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterfaceBL\n" +
|
||||
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterface\x12\x12\n" +
|
||||
"\x04desc\x18\b \x01(\tR\x04descBL\n" +
|
||||
"\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3"
|
||||
|
||||
var (
|
||||
|
||||
@@ -14,4 +14,5 @@ message Config {
|
||||
uint32 user_level = 5;
|
||||
repeated string auto_system_routing_table = 6;
|
||||
string auto_outbounds_interface = 7;
|
||||
string desc = 8;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
|
||||
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundUplink {
|
||||
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + t.tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
t.uplinkCounter = c
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (t *Handler) Init(ctx context.Context, pm policy.Manager, dispatcher routin
|
||||
if len(t.tag) > 0 && pm.ForSystem().Stats.InboundDownlink {
|
||||
statsManager := core.MustFromContext(ctx).GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + t.tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
t.downlinkCounter = c
|
||||
}
|
||||
|
||||
+60
-18
@@ -24,11 +24,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
utunControlName = "com.apple.net.utun_control"
|
||||
sysprotoControl = 2
|
||||
gateway = "169.254.10.1/30"
|
||||
utunHeaderSize = 4
|
||||
UTUN_OPT_IFNAME = 2
|
||||
utunControlName = "com.apple.net.utun_control"
|
||||
sysprotoControl = 2
|
||||
defaultDarwinGateway = "169.254.10.1/30"
|
||||
utunHeaderSize = 4
|
||||
UTUN_OPT_IFNAME = 2
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -50,6 +50,7 @@ type DarwinTun struct {
|
||||
routeMonitor *os.File
|
||||
routeMonitorOnce sync.Once
|
||||
systemRoutes []netip.Prefix
|
||||
gateway netip.Prefix
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -85,7 +86,13 @@ func NewTun(options *Config) (Tun, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = setup(options.Name, options.MTU)
|
||||
gateway, err := selectDarwinGateway(options.Gateway)
|
||||
if err != nil {
|
||||
_ = tunFile.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = setup(options.Name, options.MTU, gateway)
|
||||
if err != nil {
|
||||
_ = tunFile.Close()
|
||||
return nil, err
|
||||
@@ -96,6 +103,7 @@ func NewTun(options *Config) (Tun, error) {
|
||||
options: options,
|
||||
tunFd: int(tunFile.Fd()),
|
||||
ownsFd: true,
|
||||
gateway: gateway,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -281,24 +289,56 @@ func open(name string) (*os.File, error) {
|
||||
}
|
||||
|
||||
// setup the interface by name
|
||||
func setup(name string, MTU uint32) error {
|
||||
func setup(name string, MTU uint32, gateway netip.Prefix) error {
|
||||
if err := setMTU(name, MTU); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
* Darwin routing require tunnel type interface to have local and remote address, to be routable.
|
||||
* To simplify inevitable task, assign the interface static ip address, which in current implementation
|
||||
* is just some random ip from link-local pool, allowing to not bother about existing routing intersection.
|
||||
* To simplify inevitable task, assign the interface static ip address.
|
||||
*/
|
||||
syntheticIP, _ := netip.ParsePrefix(gateway)
|
||||
if err := setIPAddress(name, syntheticIP); err != nil {
|
||||
if err := setIPAddress(name, gateway); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func selectDarwinGateway(configured []string) (netip.Prefix, error) {
|
||||
if len(configured) == 0 {
|
||||
return netip.ParsePrefix(defaultDarwinGateway)
|
||||
}
|
||||
|
||||
for _, value := range configured {
|
||||
prefix, err := netip.ParsePrefix(value)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, xerrors.New("invalid macOS gateway ", value).Base(err)
|
||||
}
|
||||
if !prefix.Addr().Is4() {
|
||||
continue
|
||||
}
|
||||
local, ok := nextDarwinLocalIPv4(prefix)
|
||||
if !ok || !prefix.Contains(local) {
|
||||
return netip.Prefix{}, xerrors.New("macOS gateway ", value, " must contain at least one usable local IPv4 address after the gateway address")
|
||||
}
|
||||
return prefix, nil
|
||||
}
|
||||
|
||||
return netip.Prefix{}, xerrors.New("macOS gateway requires at least one IPv4 prefix")
|
||||
}
|
||||
|
||||
func nextDarwinLocalIPv4(gateway netip.Prefix) (netip.Addr, bool) {
|
||||
local4 := gateway.Addr().As4()
|
||||
for i := len(local4) - 1; i >= 0; i-- {
|
||||
local4[i]++
|
||||
if local4[i] != 0 {
|
||||
return netip.AddrFrom4(local4), true
|
||||
}
|
||||
}
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
// setMTU sets MTU on the interface by given name
|
||||
func setMTU(name string, mtu uint32) error {
|
||||
socket, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||
@@ -344,8 +384,11 @@ func setIPAddress(name string, gateway netip.Prefix) error {
|
||||
defer unix.Close(socket4)
|
||||
|
||||
// assume local ip address is next one from the remote address
|
||||
local4 := gateway.Addr().As4()
|
||||
local4[3]++
|
||||
local, ok := nextDarwinLocalIPv4(gateway)
|
||||
if !ok || !gateway.Contains(local) {
|
||||
return xerrors.New("macOS gateway ", gateway.String(), " must contain at least one usable local IPv4 address after the gateway address")
|
||||
}
|
||||
local4 := local.As4()
|
||||
|
||||
// fill the configuration for ipv4
|
||||
ifReq4 := ifAliasReq4{
|
||||
@@ -534,7 +577,7 @@ func (t *DarwinTun) setSystemRoutes() error {
|
||||
return err
|
||||
}
|
||||
for _, destination := range routes {
|
||||
if err := execDarwinRoute(unix.RTM_ADD, tunIndex, destination); err != nil {
|
||||
if err := execDarwinRoute(unix.RTM_ADD, tunIndex, destination, t.gateway); err != nil {
|
||||
_ = t.unsetSystemRoutes()
|
||||
return xerrors.New("failed to add system route ", destination).Base(err)
|
||||
}
|
||||
@@ -551,7 +594,7 @@ func (t *DarwinTun) unsetSystemRoutes() error {
|
||||
}
|
||||
for i := len(t.systemRoutes) - 1; i >= 0; i-- {
|
||||
destination := t.systemRoutes[i]
|
||||
if err := execDarwinRoute(unix.RTM_DELETE, tunIndex, destination); err != nil && !errors.Is(err, unix.ESRCH) {
|
||||
if err := execDarwinRoute(unix.RTM_DELETE, tunIndex, destination, t.gateway); err != nil && !errors.Is(err, unix.ESRCH) {
|
||||
errs = append(errs, xerrors.New("failed to delete system route ", destination).Base(err))
|
||||
}
|
||||
}
|
||||
@@ -606,7 +649,7 @@ func darwinProtectedDefaultRoutes(ipv4 bool) []netip.Prefix {
|
||||
return routes
|
||||
}
|
||||
|
||||
func execDarwinRoute(messageType int, interfaceIndex int, destination netip.Prefix) error {
|
||||
func execDarwinRoute(messageType int, interfaceIndex int, destination netip.Prefix, gateway netip.Prefix) error {
|
||||
message := route.RouteMessage{
|
||||
Type: messageType,
|
||||
Version: unix.RTM_VERSION,
|
||||
@@ -618,11 +661,10 @@ func execDarwinRoute(messageType int, interfaceIndex int, destination netip.Pref
|
||||
}
|
||||
|
||||
if destination.Addr().Is4() {
|
||||
gatewayPrefix := netip.MustParsePrefix(gateway)
|
||||
message.Addrs = []route.Addr{
|
||||
unix.RTAX_DST: &route.Inet4Addr{IP: destination.Addr().As4()},
|
||||
unix.RTAX_NETMASK: &route.Inet4Addr{IP: prefixMask4(destination.Bits())},
|
||||
unix.RTAX_GATEWAY: &route.Inet4Addr{IP: gatewayPrefix.Addr().As4()},
|
||||
unix.RTAX_GATEWAY: &route.Inet4Addr{IP: gateway.Addr().As4()},
|
||||
}
|
||||
} else {
|
||||
message.Flags &^= unix.RTF_GATEWAY
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
//go:build darwin
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSelectDarwinGatewayDefault(t *testing.T) {
|
||||
gateway, err := selectDarwinGateway(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := gateway.String(); got != defaultDarwinGateway {
|
||||
t.Fatalf("unexpected default gateway: got %s, want %s", got, defaultDarwinGateway)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectDarwinGatewayConfiguredIPv4(t *testing.T) {
|
||||
gateway, err := selectDarwinGateway([]string{"198.18.0.1/15"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := gateway.String(); got != "198.18.0.1/15" {
|
||||
t.Fatalf("unexpected gateway: got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectDarwinGatewaySkipsIPv6(t *testing.T) {
|
||||
gateway, err := selectDarwinGateway([]string{"fc00::1/64", "198.18.0.1/15"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := gateway.String(); got != "198.18.0.1/15" {
|
||||
t.Fatalf("unexpected gateway: got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectDarwinGatewayRequiresIPv4(t *testing.T) {
|
||||
if _, err := selectDarwinGateway([]string{"fc00::1/64"}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectDarwinGatewayRequiresUsableLocalAddress(t *testing.T) {
|
||||
if _, err := selectDarwinGateway([]string{"198.18.0.1/32"}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
+85
-31
@@ -26,9 +26,10 @@ type LinuxTun struct {
|
||||
options *Config
|
||||
ownsTun bool
|
||||
|
||||
systemRoutes []netlink.Route
|
||||
routeMonitorStop chan struct{}
|
||||
routeMonitorOnce sync.Once
|
||||
interfaceAddresses []netlink.Addr
|
||||
systemRoutes []netlink.Route
|
||||
routeMonitorStop chan struct{}
|
||||
routeMonitorOnce sync.Once
|
||||
}
|
||||
|
||||
// LinuxTun implements Tun
|
||||
@@ -172,7 +173,14 @@ func (t *LinuxTun) Start() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := t.setInterfaceAddresses(); err != nil {
|
||||
_ = netlink.LinkSetDown(t.tunLink)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := t.setSystemRoutes(); err != nil {
|
||||
_ = t.unsetInterfaceAddresses()
|
||||
_ = netlink.LinkSetDown(t.tunLink)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -193,6 +201,7 @@ func (t *LinuxTun) Close() error {
|
||||
})
|
||||
|
||||
_ = t.unsetSystemRoutes()
|
||||
_ = t.unsetInterfaceAddresses()
|
||||
|
||||
if t.ownsTun {
|
||||
_ = netlink.LinkSetDown(t.tunLink)
|
||||
@@ -223,6 +232,37 @@ func setinterface(network, address string, fd uintptr, iface *net.Interface) err
|
||||
return unix.BindToDevice(int(fd), iface.Name)
|
||||
}
|
||||
|
||||
func (t *LinuxTun) setInterfaceAddresses() error {
|
||||
if len(t.options.Gateway) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, address := range t.options.Gateway {
|
||||
addr, err := netlink.ParseAddr(address)
|
||||
if err != nil {
|
||||
_ = t.unsetInterfaceAddresses()
|
||||
return errors.New("invalid interface address ", address).Base(err)
|
||||
}
|
||||
if err := netlink.AddrAdd(t.tunLink, addr); err != nil {
|
||||
_ = t.unsetInterfaceAddresses()
|
||||
return errors.New("failed to add interface address ", address).Base(err)
|
||||
}
|
||||
t.interfaceAddresses = append(t.interfaceAddresses, *addr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *LinuxTun) unsetInterfaceAddresses() error {
|
||||
var errs []error
|
||||
for i := len(t.interfaceAddresses) - 1; i >= 0; i-- {
|
||||
address := t.interfaceAddresses[i]
|
||||
if err := netlink.AddrDel(t.tunLink, &address); err != nil {
|
||||
errs = append(errs, errors.New("failed to delete interface address ", address.String()).Base(err))
|
||||
}
|
||||
}
|
||||
t.interfaceAddresses = nil
|
||||
return errors.Combine(errs...)
|
||||
}
|
||||
|
||||
func (t *LinuxTun) setSystemRoutes() error {
|
||||
if len(t.options.AutoSystemRoutingTable) == 0 {
|
||||
return nil
|
||||
@@ -308,36 +348,37 @@ func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, erro
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
probeIPs := []net.IP{
|
||||
net.ParseIP("8.8.8.8"),
|
||||
net.ParseIP("2001:4860:4860::8888"),
|
||||
for _, family := range []int{
|
||||
netlink.FAMILY_V4,
|
||||
netlink.FAMILY_V6,
|
||||
} {
|
||||
iface, err := findDefaultInterface(family, tunIndex)
|
||||
if err == nil {
|
||||
return iface, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, ip := range probeIPs {
|
||||
routes, err := netlink.RouteGet(ip)
|
||||
if err != nil || len(routes) == 0 {
|
||||
continue
|
||||
}
|
||||
route := routes[0]
|
||||
if route.LinkIndex == tunIndex {
|
||||
continue
|
||||
return nil, errors.New("no usable outbound interface found")
|
||||
}
|
||||
|
||||
func findDefaultInterface(family int, tunIndex int) (*net.Interface, error) {
|
||||
routes, err := netlink.RouteList(nil, family)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var selected *net.Interface
|
||||
selectedMetric := -1
|
||||
|
||||
for _, route := range routes {
|
||||
if route.Dst != nil {
|
||||
ones, _ := route.Dst.Mask.Size()
|
||||
if ones != 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
link, err := netlink.LinkByIndex(route.LinkIndex)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
attrs := link.Attrs()
|
||||
|
||||
if attrs.Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
operState := attrs.OperState
|
||||
if operState != netlink.OperUp && operState != netlink.OperUnknown {
|
||||
continue
|
||||
}
|
||||
|
||||
if route.Src == nil || route.Src.IsLoopback() || route.Src.IsLinkLocalUnicast() {
|
||||
if route.LinkIndex == 0 || route.LinkIndex == tunIndex {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -345,8 +386,21 @@ func findOutboundInterface(tunIndex int, fixedName string) (*net.Interface, erro
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return iface, nil
|
||||
|
||||
if iface.Flags&net.FlagUp == 0 ||
|
||||
iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if selected == nil || route.Priority < selectedMetric {
|
||||
selected = iface
|
||||
selectedMetric = route.Priority
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("no usable outbound interface found")
|
||||
if selected == nil {
|
||||
return nil, errors.New("physical default route not found")
|
||||
}
|
||||
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ var _ GVisorDevice = (*WindowsTun)(nil)
|
||||
// interface with the same name exist, it tried to be reused.
|
||||
func NewTun(options *Config) (Tun, error) {
|
||||
// instantiate wintun adapter
|
||||
adapter, err := open(options.Name)
|
||||
adapter, err := open(options.Name, options.Desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -73,12 +73,12 @@ func NewTun(options *Config) (Tun, error) {
|
||||
return tun, nil
|
||||
}
|
||||
|
||||
func open(name string) (*wintun.Adapter, error) {
|
||||
func open(name, desc string) (*wintun.Adapter, error) {
|
||||
// generate a deterministic GUID from the adapter name
|
||||
id := md5.Sum([]byte(name))
|
||||
guid := (*windows.GUID)(unsafe.Pointer(&id[0]))
|
||||
// try to create adapter anew
|
||||
adapter, err := wintun.CreateAdapter(name, "Xray", guid)
|
||||
adapter, err := wintun.CreateAdapter(name, desc, guid)
|
||||
if err == nil {
|
||||
return adapter, nil
|
||||
}
|
||||
|
||||
@@ -17,27 +17,6 @@ func Authenticate(b []byte) uint32 {
|
||||
return fnv1hash.Sum32()
|
||||
}
|
||||
|
||||
// [DEPRECATED 2023-06]
|
||||
type NoOpAuthenticator struct{}
|
||||
|
||||
func (NoOpAuthenticator) NonceSize() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (NoOpAuthenticator) Overhead() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Seal implements AEAD.Seal().
|
||||
func (NoOpAuthenticator) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
|
||||
return append(dst[:0], plaintext...)
|
||||
}
|
||||
|
||||
// Open implements AEAD.Open().
|
||||
func (NoOpAuthenticator) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
|
||||
return append(dst[:0], ciphertext...), nil
|
||||
}
|
||||
|
||||
// GenerateChacha20Poly1305Key generates a 32-byte key from a given 16-byte array.
|
||||
func GenerateChacha20Poly1305Key(b []byte) []byte {
|
||||
key := make([]byte, 32)
|
||||
|
||||
@@ -116,20 +116,6 @@ func (c *ClientSession) EncodeRequestBody(request *protocol.RequestHeader, write
|
||||
}
|
||||
|
||||
switch request.Security {
|
||||
case protocol.SecurityType_NONE:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if request.Command.TransferType() == protocol.TransferTypeStream {
|
||||
return crypto.NewChunkStreamWriter(sizeParser, writer), nil
|
||||
}
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(NoOpAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding), nil
|
||||
}
|
||||
|
||||
return buf.NewWriter(writer), nil
|
||||
case protocol.SecurityType_AES128_GCM:
|
||||
aead := crypto.NewAesGcm(c.requestBodyKey[:])
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
@@ -267,22 +253,6 @@ func (c *ClientSession) DecodeResponseBody(request *protocol.RequestHeader, read
|
||||
}
|
||||
|
||||
switch request.Security {
|
||||
case protocol.SecurityType_NONE:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if request.Command.TransferType() == protocol.TransferTypeStream {
|
||||
return crypto.NewChunkStreamReader(sizeParser, reader), nil
|
||||
}
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(NoOpAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding), nil
|
||||
}
|
||||
|
||||
return buf.NewReader(reader), nil
|
||||
case protocol.SecurityType_AES128_GCM:
|
||||
aead := crypto.NewAesGcm(c.responseBodyKey[:])
|
||||
|
||||
|
||||
@@ -262,21 +262,6 @@ func (s *ServerSession) DecodeRequestBody(request *protocol.RequestHeader, reade
|
||||
}
|
||||
|
||||
switch request.Security {
|
||||
case protocol.SecurityType_NONE:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if request.Command.TransferType() == protocol.TransferTypeStream {
|
||||
return crypto.NewChunkStreamReader(sizeParser, reader), nil
|
||||
}
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(NoOpAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationReader(auth, sizeParser, reader, protocol.TransferTypePacket, padding), nil
|
||||
}
|
||||
return buf.NewReader(reader), nil
|
||||
|
||||
case protocol.SecurityType_AES128_GCM:
|
||||
aead := crypto.NewAesGcm(s.requestBodyKey[:])
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
@@ -384,21 +369,6 @@ func (s *ServerSession) EncodeResponseBody(request *protocol.RequestHeader, writ
|
||||
}
|
||||
|
||||
switch request.Security {
|
||||
case protocol.SecurityType_NONE:
|
||||
if request.Option.Has(protocol.RequestOptionChunkStream) {
|
||||
if request.Command.TransferType() == protocol.TransferTypeStream {
|
||||
return crypto.NewChunkStreamWriter(sizeParser, writer), nil
|
||||
}
|
||||
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
AEAD: new(NoOpAuthenticator),
|
||||
NonceGenerator: crypto.GenerateEmptyBytes(),
|
||||
AdditionalDataGenerator: crypto.GenerateEmptyBytes(),
|
||||
}
|
||||
return crypto.NewAuthenticationWriter(auth, sizeParser, writer, protocol.TransferTypePacket, padding), nil
|
||||
}
|
||||
return buf.NewWriter(writer), nil
|
||||
|
||||
case protocol.SecurityType_AES128_GCM:
|
||||
aead := crypto.NewAesGcm(s.responseBodyKey[:])
|
||||
auth := &crypto.AEADAuthenticator{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"hash/crc64"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
@@ -105,7 +106,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
account := request.User.Account.(*vmess.MemoryAccount)
|
||||
request.Security = account.Security
|
||||
|
||||
if request.Security == protocol.SecurityType_AES128_GCM || request.Security == protocol.SecurityType_NONE || request.Security == protocol.SecurityType_CHACHA20_POLY1305 {
|
||||
if request.Security == protocol.SecurityType_AES128_GCM || request.Security == protocol.SecurityType_CHACHA20_POLY1305 {
|
||||
request.Option.Set(protocol.RequestOptionChunkMasking)
|
||||
}
|
||||
|
||||
@@ -113,12 +114,6 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
request.Option.Set(protocol.RequestOptionGlobalPadding)
|
||||
}
|
||||
|
||||
if request.Security == protocol.SecurityType_ZERO {
|
||||
request.Security = protocol.SecurityType_NONE
|
||||
request.Option.Clear(protocol.RequestOptionChunkStream)
|
||||
request.Option.Clear(protocol.RequestOptionChunkMasking)
|
||||
}
|
||||
|
||||
if account.AuthenticatedLengthExperiment {
|
||||
request.Option.Set(protocol.RequestOptionAuthenticatedLength)
|
||||
}
|
||||
@@ -224,10 +219,17 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
return nil
|
||||
}
|
||||
|
||||
var enablePadding = false
|
||||
var enablePadding atomic.Bool
|
||||
|
||||
func shouldEnablePadding(s protocol.SecurityType) bool {
|
||||
return enablePadding || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO
|
||||
return enablePadding.Load() || s == protocol.SecurityType_AES128_GCM || s == protocol.SecurityType_CHACHA20_POLY1305 || s == protocol.SecurityType_AUTO
|
||||
}
|
||||
|
||||
func reloadEnvSettings() error {
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue })
|
||||
enablePadding.Store(paddingValue != defaultFlagValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -235,10 +237,5 @@ func init() {
|
||||
return New(ctx, config.(*Config))
|
||||
}))
|
||||
|
||||
const defaultFlagValue = "NOT_DEFINED_AT_ALL"
|
||||
|
||||
paddingValue := platform.NewEnvFlag(platform.UseVmessPadding).GetValue(func() string { return defaultFlagValue })
|
||||
if paddingValue != defaultFlagValue {
|
||||
enablePadding = true
|
||||
}
|
||||
platform.RegisterEnvReload(reloadEnvSettings)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.OutboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.OutboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "outbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
@@ -139,13 +139,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
ob.Name = "wireguard"
|
||||
ob.CanSpliceCopy = 3
|
||||
|
||||
if h.dev == nil {
|
||||
if err := h.init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.dev.Up(); err != nil {
|
||||
if err := h.init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -258,8 +252,11 @@ func (h *Handler) Close() (err error) {
|
||||
func (h *Handler) init(ctx context.Context) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.tun == nil {
|
||||
return errors.New("closed")
|
||||
}
|
||||
if h.dev != nil {
|
||||
return nil
|
||||
return h.dev.Up()
|
||||
}
|
||||
resolveFunc := h.resolveLocal
|
||||
listenFunc := func() (net.PacketConn, error) {
|
||||
|
||||
@@ -66,7 +66,7 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.InboundUplink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>uplink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
uplinkCounter = c
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func NewServer(ctx context.Context, conf *DeviceConfig) (*Server, error) {
|
||||
if len(tag) > 0 && p.ForSystem().Stats.InboundDownlink {
|
||||
statsManager := v.GetFeature(stats.ManagerType()).(stats.Manager)
|
||||
name := "inbound>>>" + tag + ">>>traffic>>>downlink"
|
||||
c, _ := stats.GetOrRegisterCounter(statsManager, name)
|
||||
c, _ := statsManager.GetOrRegisterCounter(name)
|
||||
if c != nil {
|
||||
downlinkCounter = c
|
||||
}
|
||||
|
||||
@@ -390,88 +390,3 @@ func TestShadowsocksAES128GCMUDPMux(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShadowsocksNone(t *testing.T) {
|
||||
tcpServer := tcp.Server{
|
||||
MsgProcessor: xor,
|
||||
}
|
||||
dest, err := tcpServer.Start()
|
||||
common.Must(err)
|
||||
|
||||
defer tcpServer.Close()
|
||||
|
||||
account := serial.ToTypedMessage(&shadowsocks.Account{
|
||||
Password: "shadowsocks-password",
|
||||
CipherType: shadowsocks.CipherType_NONE,
|
||||
})
|
||||
|
||||
serverPort := tcp.PickPort()
|
||||
serverConfig := &core.Config{
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
{
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&shadowsocks.ServerConfig{
|
||||
Users: []*protocol.User{{
|
||||
Account: account,
|
||||
Level: 1,
|
||||
}},
|
||||
Network: []net.Network{net.Network_TCP},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
clientPort := tcp.PickPort()
|
||||
clientConfig := &core.Config{
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
{
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}},
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
||||
RewriteAddress: net.NewIPOrDomain(dest.Address),
|
||||
RewritePort: uint32(dest.Port),
|
||||
AllowedNetworks: []net.Network{net.Network_TCP},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&shadowsocks.ClientConfig{
|
||||
Server: &protocol.ServerEndpoint{
|
||||
Address: net.NewIPOrDomain(net.LocalHostIP),
|
||||
Port: uint32(serverPort),
|
||||
User: &protocol.User{
|
||||
Account: account,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
servers, err := InitializeServerConfigs(serverConfig, clientConfig)
|
||||
common.Must(err)
|
||||
|
||||
defer CloseAllServers(servers)
|
||||
|
||||
var errGroup errgroup.Group
|
||||
for range 3 {
|
||||
errGroup.Go(testTCPConn(clientPort, 10240*1024, time.Second*20))
|
||||
}
|
||||
|
||||
if err := errGroup.Wait(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,103 +423,6 @@ func TestVMessChacha20(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMessNone(t *testing.T) {
|
||||
tcpServer := tcp.Server{
|
||||
MsgProcessor: xor,
|
||||
}
|
||||
dest, err := tcpServer.Start()
|
||||
common.Must(err)
|
||||
defer tcpServer.Close()
|
||||
|
||||
userID := protocol.NewID(uuid.New())
|
||||
serverPort := tcp.PickPort()
|
||||
serverConfig := &core.Config{
|
||||
App: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&log.Config{
|
||||
ErrorLogLevel: clog.Severity_Debug,
|
||||
ErrorLogType: log.LogType_Console,
|
||||
}),
|
||||
},
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
{
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&inbound.Config{
|
||||
User: []*protocol.User{
|
||||
{
|
||||
Account: serial.ToTypedMessage(&vmess.Account{
|
||||
Id: userID.String(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
clientPort := tcp.PickPort()
|
||||
clientConfig := &core.Config{
|
||||
App: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&log.Config{
|
||||
ErrorLogLevel: clog.Severity_Debug,
|
||||
ErrorLogType: log.LogType_Console,
|
||||
}),
|
||||
},
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
{
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}},
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
||||
RewriteAddress: net.NewIPOrDomain(dest.Address),
|
||||
RewritePort: uint32(dest.Port),
|
||||
AllowedNetworks: []net.Network{net.Network_TCP},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&outbound.Config{
|
||||
Receiver: &protocol.ServerEndpoint{
|
||||
Address: net.NewIPOrDomain(net.LocalHostIP),
|
||||
Port: uint32(serverPort),
|
||||
User: &protocol.User{
|
||||
Account: serial.ToTypedMessage(&vmess.Account{
|
||||
Id: userID.String(),
|
||||
SecuritySettings: &protocol.SecurityConfig{
|
||||
Type: protocol.SecurityType_NONE,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
servers, err := InitializeServerConfigs(serverConfig, clientConfig)
|
||||
common.Must(err)
|
||||
defer CloseAllServers(servers)
|
||||
|
||||
var errg errgroup.Group
|
||||
for range 3 {
|
||||
errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30))
|
||||
}
|
||||
if err := errg.Wait(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMessKCP(t *testing.T) {
|
||||
tcpServer := tcp.Server{
|
||||
MsgProcessor: xor,
|
||||
@@ -970,103 +873,6 @@ func TestVMessGCMMuxUDP(t *testing.T) {
|
||||
}()
|
||||
}
|
||||
|
||||
func TestVMessZero(t *testing.T) {
|
||||
tcpServer := tcp.Server{
|
||||
MsgProcessor: xor,
|
||||
}
|
||||
dest, err := tcpServer.Start()
|
||||
common.Must(err)
|
||||
defer tcpServer.Close()
|
||||
|
||||
userID := protocol.NewID(uuid.New())
|
||||
serverPort := tcp.PickPort()
|
||||
serverConfig := &core.Config{
|
||||
App: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&log.Config{
|
||||
ErrorLogLevel: clog.Severity_Debug,
|
||||
ErrorLogType: log.LogType_Console,
|
||||
}),
|
||||
},
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
{
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(serverPort)}},
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&inbound.Config{
|
||||
User: []*protocol.User{
|
||||
{
|
||||
Account: serial.ToTypedMessage(&vmess.Account{
|
||||
Id: userID.String(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&freedom.Config{
|
||||
FinalRules: []*freedom.FinalRuleConfig{{Action: freedom.RuleAction_Allow}},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
clientPort := tcp.PickPort()
|
||||
clientConfig := &core.Config{
|
||||
App: []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&log.Config{
|
||||
ErrorLogLevel: clog.Severity_Debug,
|
||||
ErrorLogType: log.LogType_Console,
|
||||
}),
|
||||
},
|
||||
Inbound: []*core.InboundHandlerConfig{
|
||||
{
|
||||
ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(clientPort)}},
|
||||
Listen: net.NewIPOrDomain(net.LocalHostIP),
|
||||
}),
|
||||
ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
|
||||
RewriteAddress: net.NewIPOrDomain(dest.Address),
|
||||
RewritePort: uint32(dest.Port),
|
||||
AllowedNetworks: []net.Network{net.Network_TCP},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{
|
||||
ProxySettings: serial.ToTypedMessage(&outbound.Config{
|
||||
Receiver: &protocol.ServerEndpoint{
|
||||
Address: net.NewIPOrDomain(net.LocalHostIP),
|
||||
Port: uint32(serverPort),
|
||||
User: &protocol.User{
|
||||
Account: serial.ToTypedMessage(&vmess.Account{
|
||||
Id: userID.String(),
|
||||
SecuritySettings: &protocol.SecurityConfig{
|
||||
Type: protocol.SecurityType_ZERO,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
servers, err := InitializeServerConfigs(serverConfig, clientConfig)
|
||||
common.Must(err)
|
||||
defer CloseAllServers(servers)
|
||||
|
||||
var errg errgroup.Group
|
||||
for range 3 {
|
||||
errg.Go(testTCPConn(clientPort, 1024*1024, time.Second*30))
|
||||
}
|
||||
if err := errg.Wait(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMessGCMLengthAuth(t *testing.T) {
|
||||
tcpServer := tcp.Server{
|
||||
MsgProcessor: xor,
|
||||
|
||||
@@ -27,9 +27,10 @@ type task struct {
|
||||
}
|
||||
|
||||
var (
|
||||
conns chan *websocket.Conn
|
||||
server *http.Server
|
||||
mu sync.Mutex
|
||||
conns chan *websocket.Conn
|
||||
server *http.Server
|
||||
currentAddr string
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
var upgrader = &websocket.Upgrader{
|
||||
@@ -47,8 +48,13 @@ func Reload() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if addr == currentAddr && (addr == "" || server != nil) {
|
||||
return
|
||||
}
|
||||
|
||||
if server != nil {
|
||||
server.Close()
|
||||
server = nil
|
||||
}
|
||||
if HasBrowserDialer() {
|
||||
for len(conns) > 0 {
|
||||
@@ -60,6 +66,7 @@ func Reload() {
|
||||
}
|
||||
conns = nil
|
||||
}
|
||||
currentAddr = addr
|
||||
if addr != "" {
|
||||
token := uuid.New()
|
||||
csrfToken := token.String()
|
||||
@@ -220,5 +227,8 @@ func CheckOK(conn *websocket.Conn) error {
|
||||
}
|
||||
|
||||
func init() {
|
||||
Reload()
|
||||
platform.RegisterEnvReload(func() error {
|
||||
Reload()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package xmc
|
||||
|
||||
// Copied from https://github.com/Tnze/go-mc/blob/539b4a3a7f030332eb58b8a946116ae7907630d2/net/CFB8/cfb8.go
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/subtle"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type cfb8 struct {
|
||||
c cipher.Block
|
||||
blockSize int
|
||||
ivPos int
|
||||
iv []byte
|
||||
de bool
|
||||
}
|
||||
|
||||
func newCFB8Decrypt(c cipher.Block, iv []byte) *cfb8 {
|
||||
return newCFB8(c, iv, true)
|
||||
}
|
||||
|
||||
func newCFB8Encrypt(c cipher.Block, iv []byte) *cfb8 {
|
||||
return newCFB8(c, iv, false)
|
||||
}
|
||||
|
||||
func newCFB8(c cipher.Block, iv []byte, de bool) *cfb8 {
|
||||
cp := make([]byte, len(iv)*3)
|
||||
copy(cp, iv)
|
||||
return &cfb8{
|
||||
c: c,
|
||||
blockSize: c.BlockSize(),
|
||||
iv: cp,
|
||||
de: de,
|
||||
}
|
||||
}
|
||||
|
||||
func (cf *cfb8) XORKeyStream(dst, src []byte) {
|
||||
if len(src) == 0 {
|
||||
return
|
||||
}
|
||||
if len(dst) < len(src) {
|
||||
panic("cfb8: output smaller than input")
|
||||
}
|
||||
|
||||
// If dst and src does not overlap in first block size,
|
||||
// and the length of src is greater than 2*blockSize,
|
||||
// we can use an optimized implementation.
|
||||
if len(src) > cf.blockSize<<1 &&
|
||||
(uintptr(unsafe.Pointer(&dst[0]))+uintptr(cf.blockSize) <= uintptr(unsafe.Pointer(&src[0])) ||
|
||||
uintptr(unsafe.Pointer(&src[0]))+uintptr(len(src)) <= uintptr(unsafe.Pointer(&dst[0]))) {
|
||||
// encrypt/decrypt first blockSize bytes
|
||||
// After this, the IV will come to the same as
|
||||
// the last blockSize of ciphertext, so
|
||||
// we can reuse them without copy.
|
||||
cf.xorKeyStream(dst, src[:cf.blockSize])
|
||||
var ciphertext []byte
|
||||
if cf.de {
|
||||
ciphertext = src
|
||||
} else {
|
||||
ciphertext = dst
|
||||
}
|
||||
dst = dst[cf.blockSize:]
|
||||
src = src[cf.blockSize:]
|
||||
iv := cf.iv
|
||||
_ = iv[0] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
var (
|
||||
i int
|
||||
val byte
|
||||
)
|
||||
dst = dst[:len(src)]
|
||||
if cf.de && // and requires to be non-overlapping at all
|
||||
uintptr(unsafe.Pointer(&dst[0])) <= uintptr(unsafe.Pointer(&src[len(src)-1])) &&
|
||||
uintptr(unsafe.Pointer(&src[0])) <= uintptr(unsafe.Pointer(&dst[len(dst)-1])) {
|
||||
for i = 0; i < len(src)-cf.blockSize; i += 1 {
|
||||
cf.c.Encrypt(dst[i:], ciphertext[i:])
|
||||
}
|
||||
subtle.XORBytes(dst, src[:i], dst)
|
||||
for ; i < len(src); i += 1 {
|
||||
cf.c.Encrypt(iv, ciphertext[i:])
|
||||
dst[i] = src[i] ^ iv[0]
|
||||
}
|
||||
} else {
|
||||
_ = ciphertext[len(src)]
|
||||
for i, val = range src {
|
||||
cf.c.Encrypt(iv, ciphertext[i:])
|
||||
dst[i] = val ^ iv[0]
|
||||
}
|
||||
// for-range does not increase i in the last loop,
|
||||
// compared to the classic for clause
|
||||
i += 1
|
||||
}
|
||||
// copy the current IV for next operation
|
||||
copy(iv, ciphertext[i:i+cf.blockSize])
|
||||
cf.ivPos = 0
|
||||
return
|
||||
}
|
||||
|
||||
cf.xorKeyStream(dst, src)
|
||||
}
|
||||
|
||||
func (cf *cfb8) xorKeyStream(dst, src []byte) {
|
||||
dst = dst[:len(src)] // remove bounds check in loop
|
||||
for i, val := range src {
|
||||
posPlusBlockSize := cf.ivPos + cf.blockSize
|
||||
// fast mod; 2*blockSize must be a non-negative integer power of 2
|
||||
tempPos := posPlusBlockSize & (cf.blockSize<<1 - 1)
|
||||
// reuse space to store encrypted block
|
||||
cf.c.Encrypt(cf.iv[tempPos:], cf.iv[cf.ivPos:])
|
||||
// Only the first byte of the encrypted block is used
|
||||
// for encryption/decryption, other bytes are ignored.
|
||||
val ^= cf.iv[tempPos]
|
||||
|
||||
if cf.ivPos == cf.blockSize<<1 {
|
||||
// bound reached; move to next round for next operation
|
||||
// copy next block to the start of the ring buffer
|
||||
copy(cf.iv, cf.iv[cf.ivPos+1:])
|
||||
// insert the encrypted byte to the end of IV
|
||||
if cf.de {
|
||||
cf.iv[cf.blockSize-1] = src[i]
|
||||
} else {
|
||||
cf.iv[cf.blockSize-1] = val
|
||||
}
|
||||
cf.ivPos = 0
|
||||
} else {
|
||||
// insert the encrypted byte to the end of IV
|
||||
if cf.de {
|
||||
cf.iv[posPlusBlockSize] = src[i]
|
||||
} else {
|
||||
cf.iv[posPlusBlockSize] = val
|
||||
}
|
||||
// move to next block
|
||||
cf.ivPos += 1
|
||||
}
|
||||
|
||||
dst[i] = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type clientConn struct {
|
||||
reader io.Reader
|
||||
writer io.Writer
|
||||
c net.Conn
|
||||
|
||||
state clientState
|
||||
|
||||
handshakeLock sync.Mutex
|
||||
lifecycleMu sync.Mutex
|
||||
closed bool
|
||||
profiles []loginProfile
|
||||
password string
|
||||
rsaPublicKey []byte
|
||||
hostname string
|
||||
paddingSchedule []paddingTurn
|
||||
packet *packetStream
|
||||
deadlines *connectionDeadlines
|
||||
}
|
||||
|
||||
type clientState int
|
||||
|
||||
var (
|
||||
clientStateHandshake clientState = 1
|
||||
clientStateProxy clientState = 2
|
||||
)
|
||||
|
||||
func newClientConn(c net.Conn, profiles []loginProfile, password string, rsaPublicKey []byte, hostname string) (*clientConn, error) {
|
||||
if len(rsaPublicKey) == 0 {
|
||||
return nil, fmt.Errorf("empty rsa public key")
|
||||
}
|
||||
if len(profiles) == 0 {
|
||||
return nil, fmt.Errorf("empty profiles")
|
||||
}
|
||||
paddingSchedule, err := newClientPaddingSchedule2612()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("select padding profile: %w", err)
|
||||
}
|
||||
return &clientConn{
|
||||
reader: bufio.NewReader(c),
|
||||
writer: c,
|
||||
c: c,
|
||||
state: clientStateHandshake,
|
||||
handshakeLock: sync.Mutex{},
|
||||
profiles: profiles,
|
||||
password: password,
|
||||
rsaPublicKey: rsaPublicKey,
|
||||
hostname: hostname,
|
||||
paddingSchedule: paddingSchedule,
|
||||
deadlines: newConnectionDeadlines(c),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *clientConn) handshake() error {
|
||||
c.handshakeLock.Lock()
|
||||
defer c.handshakeLock.Unlock()
|
||||
|
||||
if c.state != clientStateHandshake {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.deadlines.beginHandshake(); err != nil {
|
||||
return fmt.Errorf("set deadline: %w", err)
|
||||
}
|
||||
defer func() { _ = c.deadlines.endHandshake() }()
|
||||
|
||||
var (
|
||||
protocolVersion Varint = Varint(775)
|
||||
serverAddress String = String(c.hostname)
|
||||
serverPort UnsignedShort = UnsignedShort(25565)
|
||||
nextState Varint = Varint(2)
|
||||
)
|
||||
|
||||
host, portString, err := net.SplitHostPort(c.c.RemoteAddr().String())
|
||||
if err == nil {
|
||||
port, err := strconv.Atoi(portString)
|
||||
if err == nil {
|
||||
serverPort = UnsignedShort(port)
|
||||
}
|
||||
|
||||
if serverAddress == "" {
|
||||
serverAddress = String(host)
|
||||
}
|
||||
}
|
||||
|
||||
err = writePacket(c.writer, 0x00, &protocolVersion, &serverAddress, &serverPort, &nextState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write handshake packet: %w", err)
|
||||
}
|
||||
|
||||
// Login Start
|
||||
randomProfile, err := rand.Int(rand.Reader, big.NewInt(int64(len(c.profiles))))
|
||||
if err != nil {
|
||||
return fmt.Errorf("select profile: %w", err)
|
||||
}
|
||||
selectedProfile := c.profiles[randomProfile.Int64()]
|
||||
username := String(selectedProfile.Username)
|
||||
|
||||
err = writePacket(c.writer, 0x00, &username, &selectedProfile.UUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write login start: %w", err)
|
||||
}
|
||||
|
||||
// Encryption Request
|
||||
pkt, err := readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read encryption request: %w", err)
|
||||
}
|
||||
|
||||
if pkt.packetID != 0x01 {
|
||||
return fmt.Errorf("bad encrypt request packet id")
|
||||
}
|
||||
|
||||
var (
|
||||
serverId String
|
||||
publicKey Bytes
|
||||
verifyToken Bytes
|
||||
)
|
||||
|
||||
err = pkt.readFields(&serverId, &publicKey, &verifyToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read encryption request fields: %w", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(publicKey, c.rsaPublicKey) {
|
||||
return fmt.Errorf("server public key mismatch")
|
||||
}
|
||||
|
||||
k, err := x509.ParsePKIXPublicKey(publicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse server public key: %w", err)
|
||||
}
|
||||
|
||||
rsaPublicKey, ok := k.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("parse server public key: not rsa")
|
||||
}
|
||||
|
||||
sharedSecret := make([]byte, 16)
|
||||
if _, err = rand.Read(sharedSecret); err != nil {
|
||||
return fmt.Errorf("generate shared secret: %w", err)
|
||||
}
|
||||
|
||||
encryptedSharedSecret, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt shared secret: %w", err)
|
||||
}
|
||||
|
||||
verifyToken = append(verifyToken, []byte(c.password)...) // append pre-shared password
|
||||
|
||||
encryptedVerifyToken, err := rsa.EncryptPKCS1v15(rand.Reader, rsaPublicKey, verifyToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt verify token: %w", err)
|
||||
}
|
||||
|
||||
// Send Encryption Response
|
||||
err = writePacket(
|
||||
c.writer,
|
||||
0x01,
|
||||
(*Bytes)(&encryptedSharedSecret),
|
||||
(*Bytes)(&encryptedVerifyToken),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write encryption response: %w", err)
|
||||
}
|
||||
|
||||
// Enable encryption
|
||||
c.reader, err = newCryptoReader(c.reader, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new crypto reader: %w", err)
|
||||
}
|
||||
|
||||
c.writer, err = newCryptoWriter(c.writer, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new crypto writer: %w", err)
|
||||
}
|
||||
|
||||
pkt, err = readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login finished: %w", err)
|
||||
}
|
||||
if pkt.packetID == 0x00 {
|
||||
var reason String
|
||||
if readErr := pkt.readFields(&reason); readErr != nil {
|
||||
return fmt.Errorf("authentication rejected")
|
||||
}
|
||||
return fmt.Errorf("authentication rejected: %s", reason)
|
||||
}
|
||||
if pkt.packetID != 0x02 {
|
||||
return fmt.Errorf("bad login finished packet id: %d", pkt.packetID)
|
||||
}
|
||||
|
||||
receivedProfile, err := readLoginSuccess(pkt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login finished fields: %w", err)
|
||||
}
|
||||
if receivedProfile != selectedProfile {
|
||||
return fmt.Errorf("login profile mismatch")
|
||||
}
|
||||
loginAcknowledgedLength, err := writePacketWithLength(c.writer, 0x03)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write login acknowledged: %w", err)
|
||||
}
|
||||
if err = runPaddingSchedule(c.reader, c.writer, true, loginAcknowledgedLength, c.paddingSchedule); err != nil {
|
||||
return fmt.Errorf("run startup padding: %w", err)
|
||||
}
|
||||
|
||||
packet := newPacketStream(c.reader, c.writer, true)
|
||||
c.lifecycleMu.Lock()
|
||||
if c.closed {
|
||||
c.lifecycleMu.Unlock()
|
||||
packet.Stop()
|
||||
return net.ErrClosed
|
||||
}
|
||||
c.packet = packet
|
||||
c.reader = packet
|
||||
c.writer = packet
|
||||
c.state = clientStateProxy
|
||||
c.lifecycleMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clientConn) Read(b []byte) (int, error) {
|
||||
err := c.handshake()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("handshake: %w", err)
|
||||
}
|
||||
|
||||
return c.reader.Read(b)
|
||||
}
|
||||
|
||||
func (c *clientConn) Write(b []byte) (int, error) {
|
||||
err := c.handshake()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("handshake: %w", err)
|
||||
}
|
||||
|
||||
return c.writer.Write(b)
|
||||
}
|
||||
|
||||
func (c *clientConn) Close() error {
|
||||
c.lifecycleMu.Lock()
|
||||
c.closed = true
|
||||
packet := c.packet
|
||||
c.lifecycleMu.Unlock()
|
||||
if packet != nil {
|
||||
packet.Stop()
|
||||
}
|
||||
return c.c.Close()
|
||||
}
|
||||
|
||||
func (c *clientConn) LocalAddr() net.Addr {
|
||||
return c.c.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *clientConn) RemoteAddr() net.Addr {
|
||||
return c.c.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *clientConn) SetDeadline(t time.Time) error {
|
||||
return c.deadlines.setDeadline(t)
|
||||
}
|
||||
|
||||
func (c *clientConn) SetReadDeadline(t time.Time) error {
|
||||
return c.deadlines.setReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *clientConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.deadlines.setWriteDeadline(t)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) TCP() {
|
||||
}
|
||||
|
||||
func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) {
|
||||
profiles, err := profilesFromConfig(c.Profiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
cc, err := newClientConn(conn, profiles, c.Password, c.RsaPublicKey, c.Hostname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) {
|
||||
profiles, err := profilesFromConfig(c.Profiles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
cc, err := wrapConnServer(conn, profiles, c.Password, c.RsaPrivateKey, c.RsaPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minecraft finalmask: %w", err)
|
||||
}
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.5
|
||||
// source: transport/internet/finalmask/xmc/config.proto
|
||||
|
||||
package xmc
|
||||
|
||||
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 Profile struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Resolve the UUID from https://api.mojang.com/users/profiles/minecraft/{username}.
|
||||
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
|
||||
Uuid []byte `protobuf:"bytes,2,opt,name=uuid,proto3" json:"uuid,omitempty"`
|
||||
// Copy the signed textures property returned by
|
||||
// https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false.
|
||||
TexturesValue string `protobuf:"bytes,3,opt,name=textures_value,json=texturesValue,proto3" json:"textures_value,omitempty"`
|
||||
TexturesSignature string `protobuf:"bytes,4,opt,name=textures_signature,json=texturesSignature,proto3" json:"textures_signature,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Profile) Reset() {
|
||||
*x = Profile{}
|
||||
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Profile) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Profile) ProtoMessage() {}
|
||||
|
||||
func (x *Profile) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_finalmask_xmc_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 Profile.ProtoReflect.Descriptor instead.
|
||||
func (*Profile) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Profile) GetUsername() string {
|
||||
if x != nil {
|
||||
return x.Username
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Profile) GetUuid() []byte {
|
||||
if x != nil {
|
||||
return x.Uuid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Profile) GetTexturesValue() string {
|
||||
if x != nil {
|
||||
return x.TexturesValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Profile) GetTexturesSignature() string {
|
||||
if x != nil {
|
||||
return x.TexturesSignature
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"`
|
||||
RsaPrivateKey []byte `protobuf:"bytes,8,opt,name=rsa_private_key,json=rsaPrivateKey,proto3" json:"rsa_private_key,omitempty"`
|
||||
RsaPublicKey []byte `protobuf:"bytes,9,opt,name=rsa_public_key,json=rsaPublicKey,proto3" json:"rsa_public_key,omitempty"`
|
||||
Hostname string `protobuf:"bytes,10,opt,name=hostname,proto3" json:"hostname,omitempty"`
|
||||
Profiles []*Profile `protobuf:"bytes,11,rep,name=profiles,proto3" json:"profiles,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[1]
|
||||
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_finalmask_xmc_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 Config.ProtoReflect.Descriptor instead.
|
||||
func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *Config) GetPassword() string {
|
||||
if x != nil {
|
||||
return x.Password
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetRsaPrivateKey() []byte {
|
||||
if x != nil {
|
||||
return x.RsaPrivateKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetRsaPublicKey() []byte {
|
||||
if x != nil {
|
||||
return x.RsaPublicKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetHostname() string {
|
||||
if x != nil {
|
||||
return x.Hostname
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetProfiles() []*Profile {
|
||||
if x != nil {
|
||||
return x.Profiles
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_transport_internet_finalmask_xmc_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_transport_internet_finalmask_xmc_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"-transport/internet/finalmask/xmc/config.proto\x12%xray.transport.internet.finalmask.xmc\"\x8f\x01\n" +
|
||||
"\aProfile\x12\x1a\n" +
|
||||
"\busername\x18\x01 \x01(\tR\busername\x12\x12\n" +
|
||||
"\x04uuid\x18\x02 \x01(\fR\x04uuid\x12%\n" +
|
||||
"\x0etextures_value\x18\x03 \x01(\tR\rtexturesValue\x12-\n" +
|
||||
"\x12textures_signature\x18\x04 \x01(\tR\x11texturesSignature\"\xe0\x01\n" +
|
||||
"\x06Config\x12\x1a\n" +
|
||||
"\bpassword\x18\x01 \x01(\tR\bpassword\x12&\n" +
|
||||
"\x0frsa_private_key\x18\b \x01(\fR\rrsaPrivateKey\x12$\n" +
|
||||
"\x0ersa_public_key\x18\t \x01(\fR\frsaPublicKey\x12\x1a\n" +
|
||||
"\bhostname\x18\n" +
|
||||
" \x01(\tR\bhostname\x12J\n" +
|
||||
"\bprofiles\x18\v \x03(\v2..xray.transport.internet.finalmask.xmc.ProfileR\bprofilesJ\x04\b\x02\x10\x03B\x91\x01\n" +
|
||||
")com.xray.transport.internet.finalmask.xmcP\x01Z:github.com/xtls/xray-core/transport/internet/finalmask/xmc\xaa\x02%Xray.Transport.Internet.Finalmask.XMCb\x06proto3"
|
||||
|
||||
var (
|
||||
file_transport_internet_finalmask_xmc_config_proto_rawDescOnce sync.Once
|
||||
file_transport_internet_finalmask_xmc_config_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP() []byte {
|
||||
file_transport_internet_finalmask_xmc_config_proto_rawDescOnce.Do(func() {
|
||||
file_transport_internet_finalmask_xmc_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_xmc_config_proto_rawDesc), len(file_transport_internet_finalmask_xmc_config_proto_rawDesc)))
|
||||
})
|
||||
return file_transport_internet_finalmask_xmc_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_transport_internet_finalmask_xmc_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_transport_internet_finalmask_xmc_config_proto_goTypes = []any{
|
||||
(*Profile)(nil), // 0: xray.transport.internet.finalmask.xmc.Profile
|
||||
(*Config)(nil), // 1: xray.transport.internet.finalmask.xmc.Config
|
||||
}
|
||||
var file_transport_internet_finalmask_xmc_config_proto_depIdxs = []int32{
|
||||
0, // 0: xray.transport.internet.finalmask.xmc.Config.profiles:type_name -> xray.transport.internet.finalmask.xmc.Profile
|
||||
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_transport_internet_finalmask_xmc_config_proto_init() }
|
||||
func file_transport_internet_finalmask_xmc_config_proto_init() {
|
||||
if File_transport_internet_finalmask_xmc_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_finalmask_xmc_config_proto_rawDesc), len(file_transport_internet_finalmask_xmc_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_transport_internet_finalmask_xmc_config_proto_goTypes,
|
||||
DependencyIndexes: file_transport_internet_finalmask_xmc_config_proto_depIdxs,
|
||||
MessageInfos: file_transport_internet_finalmask_xmc_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_transport_internet_finalmask_xmc_config_proto = out.File
|
||||
file_transport_internet_finalmask_xmc_config_proto_goTypes = nil
|
||||
file_transport_internet_finalmask_xmc_config_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package xray.transport.internet.finalmask.xmc;
|
||||
option csharp_namespace = "Xray.Transport.Internet.Finalmask.XMC";
|
||||
option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/xmc";
|
||||
option java_package = "com.xray.transport.internet.finalmask.xmc";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Profile {
|
||||
// Resolve the UUID from https://api.mojang.com/users/profiles/minecraft/{username}.
|
||||
string username = 1;
|
||||
bytes uuid = 2;
|
||||
// Copy the signed textures property returned by
|
||||
// https://sessionserver.mojang.com/session/minecraft/profile/{uuid}?unsigned=false.
|
||||
string textures_value = 3;
|
||||
string textures_signature = 4;
|
||||
}
|
||||
|
||||
message Config {
|
||||
string password = 1;
|
||||
reserved 2;
|
||||
bytes rsa_private_key = 8;
|
||||
bytes rsa_public_key = 9;
|
||||
string hostname = 10;
|
||||
repeated Profile profiles = 11;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const handshakeTimeout = 2 * time.Minute
|
||||
|
||||
type connectionDeadlines struct {
|
||||
mu sync.Mutex
|
||||
c net.Conn
|
||||
|
||||
read time.Time
|
||||
write time.Time
|
||||
handshake time.Time
|
||||
}
|
||||
|
||||
func newConnectionDeadlines(c net.Conn) *connectionDeadlines {
|
||||
return &connectionDeadlines{c: c}
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) beginHandshake() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.handshake = time.Now().Add(handshakeTimeout)
|
||||
if err := d.applyLocked(); err != nil {
|
||||
d.handshake = time.Time{}
|
||||
_ = d.applyLocked()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) endHandshake() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.handshake = time.Time{}
|
||||
return d.applyLocked()
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) setDeadline(t time.Time) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.read = t
|
||||
d.write = t
|
||||
return d.applyLocked()
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) setReadDeadline(t time.Time) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.read = t
|
||||
return d.c.SetReadDeadline(earlierDeadline(d.read, d.handshake))
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) setWriteDeadline(t time.Time) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
d.write = t
|
||||
return d.c.SetWriteDeadline(earlierDeadline(d.write, d.handshake))
|
||||
}
|
||||
|
||||
func (d *connectionDeadlines) applyLocked() error {
|
||||
if err := d.c.SetReadDeadline(earlierDeadline(d.read, d.handshake)); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.c.SetWriteDeadline(earlierDeadline(d.write, d.handshake))
|
||||
}
|
||||
|
||||
func earlierDeadline(user, internal time.Time) time.Time {
|
||||
if internal.IsZero() {
|
||||
return user
|
||||
}
|
||||
if user.IsZero() || internal.Before(user) {
|
||||
return internal
|
||||
}
|
||||
return user
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConnectionDeadlinesRestoreCallerValues(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
recording := &deadlineRecordingConn{Conn: client}
|
||||
deadlines := newConnectionDeadlines(recording)
|
||||
callerDeadline := time.Now().Add(10 * time.Minute)
|
||||
if err := deadlines.setDeadline(callerDeadline); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := deadlines.beginHandshake(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
read, write := recording.currentDeadlines()
|
||||
if !read.Before(callerDeadline) || !write.Before(callerDeadline) {
|
||||
t.Fatalf("handshake deadlines = %s/%s, caller = %s", read, write, callerDeadline)
|
||||
}
|
||||
|
||||
shortReadDeadline := time.Now().Add(time.Second)
|
||||
if err := deadlines.setReadDeadline(shortReadDeadline); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read, _ = recording.currentDeadlines()
|
||||
if !read.Equal(shortReadDeadline) {
|
||||
t.Fatalf("read deadline = %s, want %s", read, shortReadDeadline)
|
||||
}
|
||||
|
||||
if err := deadlines.endHandshake(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read, write = recording.currentDeadlines()
|
||||
if !read.Equal(shortReadDeadline) || !write.Equal(callerDeadline) {
|
||||
t.Fatalf("restored deadlines = %s/%s, want %s/%s", read, write, shortReadDeadline, callerDeadline)
|
||||
}
|
||||
}
|
||||
|
||||
type deadlineRecordingConn struct {
|
||||
net.Conn
|
||||
mu sync.Mutex
|
||||
read time.Time
|
||||
write time.Time
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) SetDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.read = t
|
||||
c.write = t
|
||||
c.mu.Unlock()
|
||||
return c.Conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) SetReadDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.read = t
|
||||
c.mu.Unlock()
|
||||
return c.Conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) SetWriteDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.write = t
|
||||
c.mu.Unlock()
|
||||
return c.Conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *deadlineRecordingConn) currentDeadlines() (time.Time, time.Time) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.read, c.write
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
type sha256Stream struct {
|
||||
seed []byte
|
||||
counter uint64
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func newSHA256Stream(seed []byte) *sha256Stream {
|
||||
return &sha256Stream{
|
||||
seed: seed,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sha256Stream) Read(p []byte) (n int, err error) {
|
||||
for len(p) > len(s.buf) {
|
||||
h := sha256.New()
|
||||
h.Write(s.seed)
|
||||
h.Write([]byte(fmt.Sprintf("-%d", s.counter)))
|
||||
s.counter++
|
||||
s.buf = append(s.buf, h.Sum(nil)...)
|
||||
}
|
||||
n = copy(p, s.buf)
|
||||
s.buf = s.buf[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func derivePrime(stream *sha256Stream) *big.Int {
|
||||
pBytes := make([]byte, 64) // 512 bits
|
||||
_, _ = stream.Read(pBytes)
|
||||
pBytes[0] |= 0xc0 // ensure it is big enough so p*q is 1024 bits
|
||||
pBytes[63] |= 0x01 // ensure odd
|
||||
|
||||
p := new(big.Int).SetBytes(pBytes)
|
||||
for {
|
||||
if p.ProbablyPrime(20) {
|
||||
pMinus1 := new(big.Int).Sub(p, big.NewInt(1))
|
||||
e := big.NewInt(65537)
|
||||
gcd := new(big.Int).GCD(nil, nil, pMinus1, e)
|
||||
if gcd.Cmp(big.NewInt(1)) == 0 {
|
||||
return p
|
||||
}
|
||||
}
|
||||
p.Add(p, big.NewInt(2))
|
||||
}
|
||||
}
|
||||
|
||||
// DeriveRSAKey derives a 1024-bit RSA private key from a password.
|
||||
func DeriveRSAKey(password string) (*rsa.PrivateKey, error) {
|
||||
seed := []byte(password)
|
||||
|
||||
pStream := newSHA256Stream(append(seed, []byte("-p-prime")...))
|
||||
qStream := newSHA256Stream(append(seed, []byte("-q-prime")...))
|
||||
|
||||
p := derivePrime(pStream)
|
||||
q := derivePrime(qStream)
|
||||
|
||||
// ensure p != q (if they are, let's search q further)
|
||||
for p.Cmp(q) == 0 {
|
||||
q.Add(q, big.NewInt(2))
|
||||
for {
|
||||
if q.ProbablyPrime(20) {
|
||||
qMinus1 := new(big.Int).Sub(q, big.NewInt(1))
|
||||
e := big.NewInt(65537)
|
||||
gcd := new(big.Int).GCD(nil, nil, qMinus1, e)
|
||||
if gcd.Cmp(big.NewInt(1)) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
q.Add(q, big.NewInt(2))
|
||||
}
|
||||
}
|
||||
|
||||
n := new(big.Int).Mul(p, q)
|
||||
pMinus1 := new(big.Int).Sub(p, big.NewInt(1))
|
||||
qMinus1 := new(big.Int).Sub(q, big.NewInt(1))
|
||||
totient := new(big.Int).Mul(pMinus1, qMinus1)
|
||||
|
||||
e := big.NewInt(65537)
|
||||
d := new(big.Int).ModInverse(e, totient)
|
||||
if d == nil {
|
||||
return nil, fmt.Errorf("failed to compute mod inverse")
|
||||
}
|
||||
|
||||
priv := &rsa.PrivateKey{
|
||||
PublicKey: rsa.PublicKey{
|
||||
N: n,
|
||||
E: 65537,
|
||||
},
|
||||
D: d,
|
||||
Primes: []*big.Int{p, q},
|
||||
}
|
||||
priv.Precompute()
|
||||
|
||||
return priv, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeriveRSAKey(t *testing.T) {
|
||||
password := "my-very-secret-password-12345"
|
||||
|
||||
key1, err := DeriveRSAKey(password)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive key: %v", err)
|
||||
}
|
||||
|
||||
err = key1.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("key is not valid RSA key: %v", err)
|
||||
}
|
||||
|
||||
key2, err := DeriveRSAKey(password)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive key second time: %v", err)
|
||||
}
|
||||
|
||||
// Verify determinism
|
||||
if key1.D.Cmp(key2.D) != 0 || key1.N.Cmp(key2.N) != 0 {
|
||||
t.Errorf("derived keys are not identical for the same password")
|
||||
}
|
||||
|
||||
// Verify different passwords yield different keys
|
||||
keyDifferent, err := DeriveRSAKey(password + "-different")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive different key: %v", err)
|
||||
}
|
||||
|
||||
if key1.D.Cmp(keyDifferent.D) == 0 || key1.N.Cmp(keyDifferent.N) == 0 {
|
||||
t.Errorf("derived keys are identical for different passwords")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveRSAKeyGoldenPrivateKey(t *testing.T) {
|
||||
const password = "deterministic-rsa-key-golden"
|
||||
const wantPKCS1DERHash = "3a8c4ad56a6fb42dab73c4d5fc3af754460a2db1441edc0970cbc7f4e0798d2f"
|
||||
|
||||
key, err := DeriveRSAKey(password)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive key: %v", err)
|
||||
}
|
||||
|
||||
gotHash := sha256.Sum256(x509.MarshalPKCS1PrivateKey(key))
|
||||
got := hex.EncodeToString(gotHash[:])
|
||||
if got != wantPKCS1DERHash {
|
||||
t.Fatalf("derived private key changed\nwant sha256: %s\n got sha256: %s", wantPKCS1DERHash, got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func deriveTestRSAKey(t *testing.T, password string) ([]byte, []byte) {
|
||||
t.Helper()
|
||||
|
||||
key, err := DeriveRSAKey(password)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to derive rsa key: %v", err)
|
||||
}
|
||||
|
||||
publicKey, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal public key: %v", err)
|
||||
}
|
||||
|
||||
return x509.MarshalPKCS1PrivateKey(key), publicKey
|
||||
}
|
||||
|
||||
func testLoginProfile(username string) loginProfile {
|
||||
profile := loginProfile{
|
||||
Username: username,
|
||||
TexturesValue: strings.Repeat("texture-value-", 40),
|
||||
TexturesSignature: strings.Repeat("texture-signature-", 24),
|
||||
}
|
||||
digest := sha256.Sum256([]byte(username))
|
||||
copy(profile.UUID[:], digest[:16])
|
||||
profile.UUID[6] = (profile.UUID[6] & 0x0f) | 0x40
|
||||
profile.UUID[8] = (profile.UUID[8] & 0x3f) | 0x80
|
||||
return profile
|
||||
}
|
||||
|
||||
func TestHandshakeSuccess(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
password := "super-secure-shared-key-12345"
|
||||
profiles := []loginProfile{testLoginProfile("test_user")}
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
|
||||
go func() {
|
||||
rawConn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
server, err := wrapConnServer(rawConn, profiles, password, privateKey, publicKey)
|
||||
if err != nil {
|
||||
t.Errorf("failed to wrap server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := server.Read(buf)
|
||||
if err != nil {
|
||||
t.Errorf("server read error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(buf[:n], []byte("hello server")) {
|
||||
t.Errorf("unexpected payload from client: %s", string(buf[:n]))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = server.Write([]byte("hello client"))
|
||||
if err != nil {
|
||||
t.Errorf("server write error: %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
clientRaw, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial: %v", err)
|
||||
}
|
||||
defer clientRaw.Close()
|
||||
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
_, err = client.Write([]byte("hello server"))
|
||||
if err != nil {
|
||||
t.Fatalf("client write error: %v", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
n, err := client.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("client read error: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(buf[:n], []byte("hello client")) {
|
||||
t.Errorf("unexpected payload from server: %s", string(buf[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandshakePasswordMismatch(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
clientPassword := "client-secret-123"
|
||||
serverPassword := "server-secret-456"
|
||||
profiles := []loginProfile{testLoginProfile("test_user")}
|
||||
serverPrivateKey, serverPublicKey := deriveTestRSAKey(t, serverPassword)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
rawConn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
server, err := wrapConnServer(rawConn, profiles, serverPassword, serverPrivateKey, serverPublicKey)
|
||||
if err != nil {
|
||||
// Wrapping is synchronous and shouldn't fail initially simply because key derivation works with any string
|
||||
t.Logf("wrapped server: %v", err)
|
||||
}
|
||||
|
||||
// When client sends data, handshake happens and should fail
|
||||
buf := make([]byte, 1024)
|
||||
_, err = server.Read(buf)
|
||||
if err == nil {
|
||||
t.Errorf("expected handshake to fail due to password mismatch, but it succeeded")
|
||||
} else {
|
||||
t.Logf("server read failed as expected: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
clientRaw, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to dial: %v", err)
|
||||
}
|
||||
defer clientRaw.Close()
|
||||
|
||||
client, err := newClientConn(clientRaw, profiles, clientPassword, serverPublicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
err = client.handshake()
|
||||
if err == nil {
|
||||
t.Fatal("expected client handshake to fail due to password mismatch")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check if we lost connection or received error
|
||||
t.Log("Handshake mismatch tested")
|
||||
}
|
||||
|
||||
func TestHandshakeNetPipeWithKeepAlive(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "net-pipe-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("pipe_user")}
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
serverDone := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
server, err := wrapConnServer(serverRaw, profiles, password, privateKey, publicKey)
|
||||
if err != nil {
|
||||
serverDone <- err
|
||||
return
|
||||
}
|
||||
|
||||
request := make([]byte, len("hello server"))
|
||||
if _, err = io.ReadFull(server, request); err != nil {
|
||||
serverDone <- fmt.Errorf("read request: %w", err)
|
||||
return
|
||||
}
|
||||
if string(request) != "hello server" {
|
||||
serverDone <- fmt.Errorf("unexpected request: %q", request)
|
||||
return
|
||||
}
|
||||
|
||||
followupDone := make(chan error, 1)
|
||||
go func() {
|
||||
followup := make([]byte, len("after keepalive"))
|
||||
_, readErr := io.ReadFull(server, followup)
|
||||
if readErr == nil && string(followup) != "after keepalive" {
|
||||
readErr = fmt.Errorf("unexpected followup: %q", followup)
|
||||
}
|
||||
followupDone <- readErr
|
||||
}()
|
||||
|
||||
if err = server.packet.writeKeepAlive(Long(42)); err != nil {
|
||||
serverDone <- fmt.Errorf("write keep-alive: %w", err)
|
||||
return
|
||||
}
|
||||
if _, err = server.Write([]byte("hello client")); err != nil {
|
||||
serverDone <- fmt.Errorf("write response: %w", err)
|
||||
return
|
||||
}
|
||||
serverDone <- <-followupDone
|
||||
}()
|
||||
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = client.Write([]byte("hello server")); err != nil {
|
||||
t.Fatalf("write request: %v", err)
|
||||
}
|
||||
response := make([]byte, len("hello client"))
|
||||
if _, err = io.ReadFull(client, response); err != nil {
|
||||
t.Fatalf("read response: %v", err)
|
||||
}
|
||||
if string(response) != "hello client" {
|
||||
t.Fatalf("unexpected response: %q", response)
|
||||
}
|
||||
if _, err = client.Write([]byte("after keepalive")); err != nil {
|
||||
t.Fatalf("write followup: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-serverDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("net.Pipe handshake timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusQueryUnaffected(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "status-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("status_user")}
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
server, err := wrapConnServer(serverRaw, profiles, password, privateKey, publicKey)
|
||||
if err == nil {
|
||||
err = server.handshake()
|
||||
}
|
||||
serverDone <- err
|
||||
}()
|
||||
|
||||
protocolVersion := Varint(775)
|
||||
serverAddress := String("localhost")
|
||||
serverPort := UnsignedShort(25565)
|
||||
nextState := Varint(1)
|
||||
if err := writePacket(clientRaw, 0x00, &protocolVersion, &serverAddress, &serverPort, &nextState); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writePacket(clientRaw, 0x00); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, err := readPacket(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.packetID != 0x00 {
|
||||
t.Fatalf("status packet id = %d", response.packetID)
|
||||
}
|
||||
var responseJSON String
|
||||
if err = response.readFields(&responseJSON); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(responseJSON) != statusResponse {
|
||||
t.Fatalf("status response = %q", responseJSON)
|
||||
}
|
||||
|
||||
payload := Long(0x0102030405060708)
|
||||
if err = writePacket(clientRaw, 0x01, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pong, err := readPacket(clientRaw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var receivedPayload Long
|
||||
if pong.packetID != 0x01 {
|
||||
t.Fatalf("pong packet id = %d", pong.packetID)
|
||||
}
|
||||
if err = pong.readFields(&receivedPayload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if receivedPayload != payload {
|
||||
t.Fatalf("pong payload = %x", receivedPayload)
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-serverDone:
|
||||
if err == nil || !strings.Contains(err.Error(), "ping") {
|
||||
t.Fatalf("server error = %v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("status handshake timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientHandshakeHonorsCallerDeadline(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer clientRaw.Close()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "deadline-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("deadline_user")}
|
||||
_, publicKey := deriveTestRSAKey(t, password)
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = client.SetDeadline(time.Now().Add(30 * time.Millisecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
_, err = client.Write([]byte("blocked"))
|
||||
var netErr net.Error
|
||||
if !errors.As(err, &netErr) || !netErr.Timeout() {
|
||||
t.Fatalf("error = %v, want network timeout", err)
|
||||
}
|
||||
if elapsed := time.Since(started); elapsed > 500*time.Millisecond {
|
||||
t.Fatalf("caller deadline took %s", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCloseInterruptsHandshake(t *testing.T) {
|
||||
clientRaw, serverRaw := net.Pipe()
|
||||
defer serverRaw.Close()
|
||||
|
||||
const password = "close-shared-key"
|
||||
profiles := []loginProfile{testLoginProfile("close_user")}
|
||||
_, publicKey := deriveTestRSAKey(t, password)
|
||||
client, err := newClientConn(clientRaw, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, writeErr := client.Write([]byte("blocked"))
|
||||
done <- writeErr
|
||||
}()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if err = client.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-done:
|
||||
if err == nil {
|
||||
t.Fatal("handshake unexpectedly succeeded after close")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("close did not interrupt handshake")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLoginAcknowledgedPacketRejectsData(t *testing.T) {
|
||||
if err := validateLoginAcknowledgedPacket(&mcPacket{packetID: 0x03}); err != nil {
|
||||
t.Fatalf("valid login acknowledged packet: %v", err)
|
||||
}
|
||||
if err := validateLoginAcknowledgedPacket(&mcPacket{packetID: 0x03, data: []byte{0x00}}); err == nil {
|
||||
t.Fatal("login acknowledged packet with trailing data was accepted")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
configurationClientboundCustomPayload = 0x01
|
||||
configurationServerboundCustomPayload = 0x02
|
||||
configurationKeepAlive = 0x04
|
||||
|
||||
packetChannel = "xmc:data"
|
||||
maxPacketData = 24 * 1024
|
||||
keepAlivePeriod = 15 * time.Second
|
||||
)
|
||||
|
||||
// packetStream carries the raw proxy byte stream in Minecraft configuration
|
||||
// custom payload packets. The configuration state provides bidirectional
|
||||
// payload packets and keep-alives without requiring version-specific world data.
|
||||
type packetStream struct {
|
||||
reader io.Reader
|
||||
writer io.Writer
|
||||
isClient bool
|
||||
|
||||
readMu sync.Mutex
|
||||
writeMu sync.Mutex
|
||||
pending []byte
|
||||
|
||||
keepAliveID atomic.Int64
|
||||
done chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func newPacketStream(reader io.Reader, writer io.Writer, isClient bool) *packetStream {
|
||||
s := &packetStream{
|
||||
reader: reader,
|
||||
writer: writer,
|
||||
isClient: isClient,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
if !isClient {
|
||||
go s.keepAliveLoop()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *packetStream) Read(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
s.readMu.Lock()
|
||||
defer s.readMu.Unlock()
|
||||
|
||||
if len(s.pending) > 0 {
|
||||
n := copy(p, s.pending)
|
||||
s.pending = s.pending[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
for {
|
||||
packet, err := readPacket(s.reader)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read minecraft packet stream: %w", err)
|
||||
}
|
||||
|
||||
if packet.packetID == s.remoteCustomPayloadID() {
|
||||
payload, ok, err := parseCustomPayload(packet)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !ok || len(payload) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
n := copy(p, payload)
|
||||
if n < len(payload) {
|
||||
s.pending = append(s.pending[:0], payload[n:]...)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
if packet.packetID == configurationKeepAlive {
|
||||
var id Long
|
||||
if err := packet.readFields(&id); err != nil {
|
||||
return 0, fmt.Errorf("read minecraft keep-alive: %w", err)
|
||||
}
|
||||
if s.isClient {
|
||||
if err := s.writeKeepAlive(id); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *packetStream) Write(p []byte) (int, error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
|
||||
written := 0
|
||||
for written < len(p) {
|
||||
end := written + maxPacketData
|
||||
if end > len(p) {
|
||||
end = len(p)
|
||||
}
|
||||
channel := String(packetChannel)
|
||||
payload := RestBytes(p[written:end])
|
||||
if err := writePacket(s.writer, s.localCustomPayloadID(), &channel, &payload); err != nil {
|
||||
return written, fmt.Errorf("write minecraft custom payload: %w", err)
|
||||
}
|
||||
written = end
|
||||
}
|
||||
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func (s *packetStream) Stop() {
|
||||
s.stopOnce.Do(func() { close(s.done) })
|
||||
}
|
||||
|
||||
func (s *packetStream) localCustomPayloadID() int {
|
||||
if s.isClient {
|
||||
return configurationServerboundCustomPayload
|
||||
}
|
||||
return configurationClientboundCustomPayload
|
||||
}
|
||||
|
||||
func (s *packetStream) remoteCustomPayloadID() int {
|
||||
if s.isClient {
|
||||
return configurationClientboundCustomPayload
|
||||
}
|
||||
return configurationServerboundCustomPayload
|
||||
}
|
||||
|
||||
func parseCustomPayload(packet *mcPacket) ([]byte, bool, error) {
|
||||
r := bytes.NewReader(packet.data)
|
||||
var channel String
|
||||
if err := channel.readFrom(r); err != nil {
|
||||
return nil, false, fmt.Errorf("read minecraft custom payload channel: %w", err)
|
||||
}
|
||||
if string(channel) != packetChannel {
|
||||
return nil, false, nil
|
||||
}
|
||||
payload := make([]byte, r.Len())
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return nil, false, fmt.Errorf("read minecraft custom payload data: %w", err)
|
||||
}
|
||||
return payload, true, nil
|
||||
}
|
||||
|
||||
func (s *packetStream) writeKeepAlive(id Long) error {
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
if err := writePacket(s.writer, configurationKeepAlive, &id); err != nil {
|
||||
return fmt.Errorf("write minecraft keep-alive: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *packetStream) keepAliveLoop() {
|
||||
ticker := time.NewTicker(keepAlivePeriod)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
id := Long(s.keepAliveID.Add(1))
|
||||
if err := s.writeKeepAlive(id); err != nil {
|
||||
return
|
||||
}
|
||||
case <-s.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPacketStreamUsesPlainFraming(t *testing.T) {
|
||||
payload := []byte("hello")
|
||||
var wire bytes.Buffer
|
||||
stream := newPacketStream(bytes.NewReader(nil), &wire, true)
|
||||
|
||||
written, err := stream.Write(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("write payload: %v", err)
|
||||
}
|
||||
if written != len(payload) {
|
||||
t.Fatalf("written = %d, want %d", written, len(payload))
|
||||
}
|
||||
wantOutbound := []byte{0x0f, 0x02, 0x08, 'x', 'm', 'c', ':', 'd', 'a', 't', 'a', 'h', 'e', 'l', 'l', 'o'}
|
||||
if !bytes.Equal(wire.Bytes(), wantOutbound) {
|
||||
t.Fatalf("wire frame = %x, want %x", wire.Bytes(), wantOutbound)
|
||||
}
|
||||
|
||||
wantInbound := append([]byte(nil), wantOutbound...)
|
||||
wantInbound[1] = configurationClientboundCustomPayload
|
||||
reader := newPacketStream(bytes.NewReader(wantInbound), io.Discard, true)
|
||||
got := make([]byte, len(payload))
|
||||
if _, err = io.ReadFull(reader, got); err != nil {
|
||||
t.Fatalf("read payload: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("payload = %q, want %q", got, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketStreamRoundTrip(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
const password = "packet-stream-shared-key"
|
||||
privateKey, publicKey := deriveTestRSAKey(t, password)
|
||||
profiles := []loginProfile{testLoginProfile("packet_user")}
|
||||
clientPayload := bytes.Repeat([]byte("client-payload-"), 5000)
|
||||
serverPayload := bytes.Repeat([]byte("server-payload-"), 5000)
|
||||
serverDone := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
rawConn, acceptErr := ln.Accept()
|
||||
if acceptErr != nil {
|
||||
serverDone <- acceptErr
|
||||
return
|
||||
}
|
||||
defer rawConn.Close()
|
||||
|
||||
server, wrapErr := wrapConnServer(rawConn, profiles, password, privateKey, publicKey)
|
||||
if wrapErr != nil {
|
||||
serverDone <- wrapErr
|
||||
return
|
||||
}
|
||||
got := make([]byte, len(clientPayload))
|
||||
if _, readErr := io.ReadFull(server, got); readErr != nil {
|
||||
serverDone <- readErr
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(got, clientPayload) {
|
||||
serverDone <- io.ErrUnexpectedEOF
|
||||
return
|
||||
}
|
||||
_, writeErr := server.Write(serverPayload)
|
||||
serverDone <- writeErr
|
||||
}()
|
||||
|
||||
rawClient, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rawClient.Close()
|
||||
|
||||
client, err := newClientConn(rawClient, profiles, password, publicKey, "localhost")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = client.Write(clientPayload); err != nil {
|
||||
t.Fatalf("write payload: %v", err)
|
||||
}
|
||||
got := make([]byte, len(serverPayload))
|
||||
if _, err = io.ReadFull(client, got); err != nil {
|
||||
t.Fatalf("read payload: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, serverPayload) {
|
||||
t.Fatal("server payload mismatch")
|
||||
}
|
||||
if err = <-serverDone; err != nil {
|
||||
t.Fatalf("server: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
type paddingDirection uint8
|
||||
|
||||
const (
|
||||
paddingClientToServer paddingDirection = iota + 1
|
||||
paddingServerToClient
|
||||
|
||||
paddingBufferLength = 16 * 1024
|
||||
maxPaddingChunkLength = 48 * 1024
|
||||
maxPaddingTurnLength = 8 * 1024 * 1024
|
||||
)
|
||||
|
||||
type paddingVariant struct {
|
||||
chunks []int
|
||||
delays []paddingDelayRange
|
||||
}
|
||||
|
||||
type paddingDelayRange struct {
|
||||
min time.Duration
|
||||
max time.Duration
|
||||
}
|
||||
|
||||
type paddingTurn struct {
|
||||
direction paddingDirection
|
||||
minLength int
|
||||
maxLength int
|
||||
variants []paddingVariant
|
||||
startDelay paddingDelayRange
|
||||
chunkDelay paddingDelayRange
|
||||
writeChunkMinLength int
|
||||
writeChunkLength int
|
||||
sendMinLength int
|
||||
sendMaxLength int
|
||||
sendVariants []int
|
||||
}
|
||||
|
||||
func runPaddingSchedule(reader io.Reader, writer io.Writer, isClient bool, firstTurnPrefixLength int, schedule []paddingTurn) error {
|
||||
if err := validatePaddingSchedule(schedule, firstTurnPrefixLength); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var writeBuffer []byte
|
||||
for i, turn := range schedule {
|
||||
prefixLength := 0
|
||||
if i == 0 {
|
||||
prefixLength = firstTurnPrefixLength
|
||||
}
|
||||
|
||||
localSends := isClient == (turn.direction == paddingClientToServer)
|
||||
if localSends {
|
||||
if err := writePaddingTurnWithBuffer(writer, turn, prefixLength, time.Sleep, &writeBuffer); err != nil {
|
||||
return fmt.Errorf("write padding turn %d: %w", i, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := readPaddingTurn(reader, turn, prefixLength); err != nil {
|
||||
return fmt.Errorf("read padding turn %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePaddingSchedule(schedule []paddingTurn, firstTurnPrefixLength int) error {
|
||||
if len(schedule) == 0 {
|
||||
return fmt.Errorf("empty padding schedule")
|
||||
}
|
||||
if firstTurnPrefixLength < 0 {
|
||||
return fmt.Errorf("negative first turn prefix length: %d", firstTurnPrefixLength)
|
||||
}
|
||||
if firstTurnPrefixLength > 0 && schedule[0].direction != paddingClientToServer {
|
||||
return fmt.Errorf("first prefixed padding turn is not client-to-server")
|
||||
}
|
||||
|
||||
for i, turn := range schedule {
|
||||
if turn.direction != paddingClientToServer && turn.direction != paddingServerToClient {
|
||||
return fmt.Errorf("padding turn %d has invalid direction: %d", i, turn.direction)
|
||||
}
|
||||
if err := validatePaddingDelayRange(turn.startDelay); err != nil {
|
||||
return fmt.Errorf("padding turn %d has an invalid start delay: %w", i, err)
|
||||
}
|
||||
if err := validatePaddingDelayRange(turn.chunkDelay); err != nil {
|
||||
return fmt.Errorf("padding turn %d has an invalid chunk delay: %w", i, err)
|
||||
}
|
||||
if turn.writeChunkMinLength < 0 || turn.writeChunkLength < turn.writeChunkMinLength || turn.writeChunkLength > maxPaddingChunkLength {
|
||||
return fmt.Errorf("padding turn %d has an invalid write chunk range: %d-%d", i, turn.writeChunkMinLength, turn.writeChunkLength)
|
||||
}
|
||||
if len(turn.variants) > 0 && turn.writeChunkLength != 0 {
|
||||
return fmt.Errorf("padding turn %d combines variants with generated write chunks", i)
|
||||
}
|
||||
|
||||
minLength, maxLength, err := paddingTurnBounds(turn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("padding turn %d: %w", i, err)
|
||||
}
|
||||
hasSendRange := turn.sendMinLength != 0 || turn.sendMaxLength != 0
|
||||
if hasSendRange {
|
||||
if len(turn.variants) > 0 {
|
||||
return fmt.Errorf("padding turn %d combines variants with a send range", i)
|
||||
}
|
||||
if turn.sendMinLength < minLength || turn.sendMaxLength < turn.sendMinLength || turn.sendMaxLength > maxLength {
|
||||
return fmt.Errorf("padding turn %d has an invalid send range: %d-%d", i, turn.sendMinLength, turn.sendMaxLength)
|
||||
}
|
||||
}
|
||||
if i == 0 && minLength-firstTurnPrefixLength < 1 {
|
||||
return fmt.Errorf("padding turn 0 is too short for %d prefix bytes", firstTurnPrefixLength)
|
||||
}
|
||||
if i == 0 && len(turn.variants) > 0 {
|
||||
for j, variant := range turn.variants {
|
||||
if _, _, err = trimPaddingPrefix(variant, firstTurnPrefixLength); err != nil {
|
||||
return fmt.Errorf("padding turn 0 variant %d: %w", j, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if i > 0 && turn.direction == schedule[i-1].direction {
|
||||
return fmt.Errorf("padding turns %d and %d have the same direction", i-1, i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writePaddingTurn(w io.Writer, turn paddingTurn, prefixLength int) error {
|
||||
return writePaddingTurnWithSleep(w, turn, prefixLength, time.Sleep)
|
||||
}
|
||||
|
||||
func writePaddingTurnWithSleep(w io.Writer, turn paddingTurn, prefixLength int, sleep func(time.Duration)) error {
|
||||
return writePaddingTurnWithBuffer(w, turn, prefixLength, sleep, nil)
|
||||
}
|
||||
|
||||
func writePaddingTurnWithBuffer(w io.Writer, turn paddingTurn, prefixLength int, sleep func(time.Duration), reusableBuffer *[]byte) error {
|
||||
startDelay, err := randomPaddingDelay(turn.startDelay)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select padding start delay: %w", err)
|
||||
}
|
||||
if startDelay > 0 {
|
||||
sleep(startDelay)
|
||||
}
|
||||
|
||||
targetLength, chunks, delays, err := selectPaddingVariant(turn, prefixLength)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordLength := targetLength - prefixLength
|
||||
if recordLength < 1 {
|
||||
return fmt.Errorf("target length %d leaves an invalid record length %d", targetLength, recordLength)
|
||||
}
|
||||
|
||||
encodedLength := Varint(recordLength)
|
||||
var header bytes.Buffer
|
||||
if err = encodedLength.writeTo(&header); err != nil {
|
||||
return fmt.Errorf("write padding header: %w", err)
|
||||
}
|
||||
if len(chunks) == 0 {
|
||||
writeChunkLength := turn.writeChunkLength
|
||||
if writeChunkLength == 0 {
|
||||
writeChunkLength = paddingBufferLength
|
||||
} else if turn.writeChunkMinLength > 0 {
|
||||
writeChunkLength, err = randomPaddingTarget(turn.writeChunkMinLength, writeChunkLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("select padding write chunk length: %w", err)
|
||||
}
|
||||
}
|
||||
chunks = defaultPaddingChunks(recordLength, writeChunkLength)
|
||||
delays = make([]paddingDelayRange, len(chunks))
|
||||
for i := 1; i < len(delays); i++ {
|
||||
delays[i] = turn.chunkDelay
|
||||
}
|
||||
}
|
||||
if chunks[0] < header.Len() {
|
||||
return fmt.Errorf("first padding chunk %d is shorter than header %d", chunks[0], header.Len())
|
||||
}
|
||||
|
||||
maxChunkLength := 0
|
||||
for _, chunkLength := range chunks {
|
||||
if chunkLength < 1 || chunkLength > maxPaddingChunkLength {
|
||||
return fmt.Errorf("invalid padding chunk length: %d", chunkLength)
|
||||
}
|
||||
maxChunkLength = max(maxChunkLength, chunkLength)
|
||||
}
|
||||
var buffer []byte
|
||||
if reusableBuffer == nil {
|
||||
buffer = make([]byte, maxChunkLength)
|
||||
} else {
|
||||
if cap(*reusableBuffer) < maxChunkLength {
|
||||
*reusableBuffer = make([]byte, maxChunkLength)
|
||||
}
|
||||
buffer = (*reusableBuffer)[:maxChunkLength]
|
||||
clear(buffer)
|
||||
}
|
||||
copy(buffer, header.Bytes())
|
||||
written := 0
|
||||
for i, chunkLength := range chunks {
|
||||
if i < len(delays) {
|
||||
delay, delayErr := randomPaddingDelay(delays[i])
|
||||
if delayErr != nil {
|
||||
return fmt.Errorf("select padding chunk %d delay: %w", i, delayErr)
|
||||
}
|
||||
if delay > 0 {
|
||||
sleep(delay)
|
||||
}
|
||||
}
|
||||
if err = writeFull(w, buffer[:chunkLength]); err != nil {
|
||||
return fmt.Errorf("write padding chunk %d: %w", i, err)
|
||||
}
|
||||
written += chunkLength
|
||||
if i == 0 {
|
||||
clear(buffer[:header.Len()])
|
||||
}
|
||||
}
|
||||
if written != recordLength {
|
||||
return fmt.Errorf("padding chunks total %d, want %d", written, recordLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPaddingTurn(r io.Reader, turn paddingTurn, prefixLength int) error {
|
||||
encodedLength, headerLength, err := readVarintWithLength(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read padding header: %w", err)
|
||||
}
|
||||
recordLength := int(encodedLength)
|
||||
if recordLength < headerLength || recordLength > maxPaddingTurnLength {
|
||||
return fmt.Errorf("invalid padding record length: %d", recordLength)
|
||||
}
|
||||
totalLength := prefixLength + recordLength
|
||||
if !paddingTurnAcceptsLength(turn, totalLength) {
|
||||
if len(turn.variants) > 0 {
|
||||
return fmt.Errorf("padding turn length %d is not an allowed variant", totalLength)
|
||||
}
|
||||
return fmt.Errorf("padding turn length %d is outside %d-%d", totalLength, turn.minLength, turn.maxLength)
|
||||
}
|
||||
|
||||
var buffer [paddingBufferLength]byte
|
||||
remaining := recordLength - headerLength
|
||||
for remaining > 0 {
|
||||
chunkLength := min(remaining, len(buffer))
|
||||
if _, err := io.ReadFull(r, buffer[:chunkLength]); err != nil {
|
||||
return fmt.Errorf("read padding body: %w", err)
|
||||
}
|
||||
remaining -= chunkLength
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func selectPaddingVariant(turn paddingTurn, prefixLength int) (int, []int, []paddingDelayRange, error) {
|
||||
if len(turn.variants) == 0 {
|
||||
minimum, maximum := turn.minLength, turn.maxLength
|
||||
if turn.sendMinLength != 0 || turn.sendMaxLength != 0 {
|
||||
minimum, maximum = turn.sendMinLength, turn.sendMaxLength
|
||||
}
|
||||
targetLength, err := randomPaddingTarget(minimum, maximum)
|
||||
return targetLength, nil, nil, err
|
||||
}
|
||||
|
||||
indices := turn.sendVariants
|
||||
if len(indices) == 0 {
|
||||
indices = make([]int, len(turn.variants))
|
||||
for i := range indices {
|
||||
indices[i] = i
|
||||
}
|
||||
}
|
||||
selected, err := randomPaddingIndex(len(indices))
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
variantIndex := indices[selected]
|
||||
if variantIndex < 0 || variantIndex >= len(turn.variants) {
|
||||
return 0, nil, nil, fmt.Errorf("invalid send variant index: %d", variantIndex)
|
||||
}
|
||||
variant := turn.variants[variantIndex]
|
||||
targetLength := paddingVariantLength(variant)
|
||||
chunks, delays, err := trimPaddingPrefix(variant, prefixLength)
|
||||
if err != nil {
|
||||
return 0, nil, nil, err
|
||||
}
|
||||
return targetLength, chunks, delays, nil
|
||||
}
|
||||
|
||||
func trimPaddingPrefix(variant paddingVariant, prefixLength int) ([]int, []paddingDelayRange, error) {
|
||||
remainingPrefix := prefixLength
|
||||
firstChunk := 0
|
||||
for firstChunk < len(variant.chunks) && remainingPrefix > 0 {
|
||||
chunkLength := variant.chunks[firstChunk]
|
||||
if remainingPrefix < chunkLength {
|
||||
return nil, nil, fmt.Errorf("prefix length %d splits chunk %d", prefixLength, firstChunk)
|
||||
}
|
||||
remainingPrefix -= chunkLength
|
||||
firstChunk++
|
||||
}
|
||||
if remainingPrefix != 0 || firstChunk == len(variant.chunks) {
|
||||
return nil, nil, fmt.Errorf("prefix length %d leaves no padding record", prefixLength)
|
||||
}
|
||||
|
||||
chunks := append([]int(nil), variant.chunks[firstChunk:]...)
|
||||
delays := make([]paddingDelayRange, len(chunks))
|
||||
if len(variant.delays) > 0 {
|
||||
copy(delays, variant.delays[firstChunk:])
|
||||
}
|
||||
return chunks, delays, nil
|
||||
}
|
||||
|
||||
func defaultPaddingChunks(recordLength, writeChunkLength int) []int {
|
||||
chunks := make([]int, 0, (recordLength+writeChunkLength-1)/writeChunkLength)
|
||||
for remaining := recordLength; remaining > 0; {
|
||||
chunkLength := min(remaining, writeChunkLength)
|
||||
chunks = append(chunks, chunkLength)
|
||||
remaining -= chunkLength
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
func paddingTurnBounds(turn paddingTurn) (int, int, error) {
|
||||
if len(turn.variants) == 0 {
|
||||
if turn.minLength < 1 || turn.maxLength < turn.minLength || turn.maxLength > maxPaddingTurnLength {
|
||||
return 0, 0, fmt.Errorf("invalid range: %d-%d", turn.minLength, turn.maxLength)
|
||||
}
|
||||
return turn.minLength, turn.maxLength, nil
|
||||
}
|
||||
if turn.minLength != 0 || turn.maxLength != 0 {
|
||||
return 0, 0, fmt.Errorf("variants cannot be combined with a length range")
|
||||
}
|
||||
|
||||
minLength := maxPaddingTurnLength + 1
|
||||
maxLength := 0
|
||||
for i, variant := range turn.variants {
|
||||
if len(variant.chunks) == 0 {
|
||||
return 0, 0, fmt.Errorf("variant %d has no chunks", i)
|
||||
}
|
||||
if len(variant.delays) != 0 && len(variant.delays) != len(variant.chunks) {
|
||||
return 0, 0, fmt.Errorf("variant %d has %d chunks and %d delays", i, len(variant.chunks), len(variant.delays))
|
||||
}
|
||||
for j, chunkLength := range variant.chunks {
|
||||
if chunkLength < 1 || chunkLength > maxPaddingChunkLength {
|
||||
return 0, 0, fmt.Errorf("variant %d chunk %d has invalid length: %d", i, j, chunkLength)
|
||||
}
|
||||
if len(variant.delays) > 0 {
|
||||
if err := validatePaddingDelayRange(variant.delays[j]); err != nil {
|
||||
return 0, 0, fmt.Errorf("variant %d chunk %d has an invalid delay: %w", i, j, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
length := paddingVariantLength(variant)
|
||||
if length > maxPaddingTurnLength {
|
||||
return 0, 0, fmt.Errorf("variant %d is too long: %d", i, length)
|
||||
}
|
||||
minLength = min(minLength, length)
|
||||
maxLength = max(maxLength, length)
|
||||
}
|
||||
for _, index := range turn.sendVariants {
|
||||
if index < 0 || index >= len(turn.variants) {
|
||||
return 0, 0, fmt.Errorf("invalid send variant index: %d", index)
|
||||
}
|
||||
}
|
||||
return minLength, maxLength, nil
|
||||
}
|
||||
|
||||
func paddingTurnAcceptsLength(turn paddingTurn, length int) bool {
|
||||
if len(turn.variants) == 0 {
|
||||
return length >= turn.minLength && length <= turn.maxLength
|
||||
}
|
||||
for _, variant := range turn.variants {
|
||||
if paddingVariantLength(variant) == length {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func paddingVariantLength(variant paddingVariant) int {
|
||||
total := 0
|
||||
for _, chunkLength := range variant.chunks {
|
||||
total += chunkLength
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
func validatePaddingDelayRange(delay paddingDelayRange) error {
|
||||
if delay.min < 0 || delay.max < delay.min {
|
||||
return fmt.Errorf("invalid range: %s-%s", delay.min, delay.max)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomPaddingDelay(delay paddingDelayRange) (time.Duration, error) {
|
||||
if err := validatePaddingDelayRange(delay); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if delay.min == delay.max {
|
||||
return delay.min, nil
|
||||
}
|
||||
span := int64(delay.max-delay.min) + 1
|
||||
offset, err := rand.Int(rand.Reader, big.NewInt(span))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select padding delay: %w", err)
|
||||
}
|
||||
return delay.min + time.Duration(offset.Int64()), nil
|
||||
}
|
||||
|
||||
func randomPaddingIndex(length int) (int, error) {
|
||||
if length < 1 {
|
||||
return 0, fmt.Errorf("select from empty padding choices")
|
||||
}
|
||||
if length == 1 {
|
||||
return 0, nil
|
||||
}
|
||||
index, err := rand.Int(rand.Reader, big.NewInt(int64(length)))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select padding choice: %w", err)
|
||||
}
|
||||
return int(index.Int64()), nil
|
||||
}
|
||||
|
||||
func randomPaddingTarget(minLength, maxLength int) (int, error) {
|
||||
if minLength == maxLength {
|
||||
return minLength, nil
|
||||
}
|
||||
span := int64(maxLength-minLength) + 1
|
||||
offset, err := rand.Int(rand.Reader, big.NewInt(span))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select padding length: %w", err)
|
||||
}
|
||||
return minLength + int(offset.Int64()), nil
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Length and write-boundary templates come from controlled Minecraft 26.1.2
|
||||
// logins. Timing deliberately uses broad random bands that preserve only the
|
||||
// rough ordering of short and long phases; it does not replay captured delays.
|
||||
var startupPaddingSchedule2612 = []paddingTurn{
|
||||
{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(2, 26, 16),
|
||||
},
|
||||
},
|
||||
{
|
||||
direction: paddingServerToClient,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(26, 21, 25),
|
||||
},
|
||||
startDelay: millisecondRange(0, 20),
|
||||
},
|
||||
{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(25),
|
||||
},
|
||||
startDelay: millisecondRange(2, 22),
|
||||
},
|
||||
{
|
||||
direction: paddingServerToClient,
|
||||
variants: []paddingVariant{
|
||||
registryPaddingVariant(),
|
||||
},
|
||||
startDelay: millisecondRange(20, 50),
|
||||
},
|
||||
{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{
|
||||
paddingVariantFromChunks(2),
|
||||
},
|
||||
startDelay: millisecondRange(10, 35),
|
||||
},
|
||||
{
|
||||
direction: paddingServerToClient,
|
||||
variants: []paddingVariant{
|
||||
playStartPaddingVariant(4941, 252, 259, 267, 268, 251, 303, 259, 264, 54, 346),
|
||||
playStartPaddingVariant(4941, 262, 284, 272, 260, 260, 313, 264, 151, 224, 207, 215, 224, 390),
|
||||
playStartPaddingVariant(4941, 257, 272, 275, 260, 260, 313, 283, 274, 226, 207, 230, 215, 204, 221, 352),
|
||||
playStartPaddingVariant(4941, 259, 272, 288, 260, 260, 311, 270, 70, 236, 223, 201, 210, 352),
|
||||
playStartPaddingVariant(4941, 255, 269, 277, 263, 260, 136, 207, 210, 232, 325),
|
||||
playStartPaddingVariant(4941, 259, 270, 274, 263, 258, 327, 170, 210, 375),
|
||||
playStartPaddingVariant(4941, 257, 275, 291, 260, 260, 325, 269, 70, 230, 226, 207, 221, 352),
|
||||
playStartPaddingVariant(4941, 252, 273, 262, 252, 254, 306, 93),
|
||||
playStartPaddingVariant(4941, 273, 270, 269, 258, 256, 322, 221, 207, 215, 438),
|
||||
playStartPaddingVariant(4941, 259, 275, 274, 250, 258, 308, 267, 154, 233, 209, 207, 213, 393),
|
||||
playStartPaddingVariant(4941, 254, 267, 272, 260, 253, 311, 167, 204, 232, 207, 481, 8),
|
||||
playStartPaddingVariant(4941, 259, 269, 272, 261, 313, 207, 213, 500, 19),
|
||||
playStartPaddingVariant(4941, 262, 269, 274, 263, 274, 311, 270, 242, 210, 229, 221, 210, 431),
|
||||
playStartPaddingVariant(4941, 259, 265, 277, 263, 277, 316, 269, 156, 204, 210, 226, 207, 413),
|
||||
playStartPaddingVariant(4941, 215, 251, 249, 317, 260, 270, 249, 52),
|
||||
playStartPaddingVariant(4941, 224, 263, 277, 316, 267, 272, 260, 138, 230, 226, 207, 204, 352),
|
||||
playStartPaddingVariant(4941, 221, 258, 263, 319, 269, 288, 263, 136, 204, 210, 220, 207, 378),
|
||||
playStartPaddingVariant(4941, 221, 258, 260, 316, 273, 291, 226, 204, 229, 213, 489, 8),
|
||||
playStartPaddingVariant(4941, 238, 260, 261, 306, 272, 277, 260, 224, 241, 212, 207, 204, 393),
|
||||
playStartPaddingVariant(4941, 224, 260, 260, 309, 272, 277, 277, 138, 207, 207, 212, 241, 352),
|
||||
},
|
||||
startDelay: millisecondRange(35, 50),
|
||||
},
|
||||
}
|
||||
|
||||
// These turns cover the finite Play-state tail through the client's
|
||||
// player_loaded packet. Bounds are the observed per-turn minima and maxima
|
||||
// across 20 controlled 26.1.2 logins; payload bytes remain opaque padding.
|
||||
var playJoinPaddingSchedule2612 = []paddingTurn{
|
||||
clientPlayPaddingTurn(6, 883),
|
||||
serverPlayPaddingTurn(346, 58638),
|
||||
clientPlayPaddingTurn(6, 887),
|
||||
serverPlayPaddingTurn(388, 61077),
|
||||
clientPlayPaddingTurn(2, 50),
|
||||
serverPlayPaddingTurn(575, 65584),
|
||||
clientPlayPaddingTurn(6, 45),
|
||||
serverPlayPaddingTurn(86, 63563),
|
||||
clientPlayPaddingTurn(2, 44),
|
||||
serverPlayPaddingTurn(42, 51983),
|
||||
clientPlayPaddingTurn(2, 851),
|
||||
serverPlayPaddingTurn(309, 25083),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(74, 63885),
|
||||
clientPlayPaddingTurn(8, 24),
|
||||
serverPlayPaddingTurn(30, 66128),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(26, 35818),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
serverPlayPaddingTurn(35, 59407),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
serverPlayPaddingTurn(37, 65328),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(26, 60622),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
serverPlayPaddingTurn(11, 60808),
|
||||
clientPlayPaddingTurn(8, 43),
|
||||
serverPlayPaddingTurn(55, 62027),
|
||||
clientPlayPaddingTurn(2, 19),
|
||||
serverPlayPaddingTurn(427, 65622),
|
||||
clientPlayPaddingTurn(5, 19),
|
||||
serverPlayPaddingTurn(35, 59401),
|
||||
clientPlayPaddingTurn(6, 19),
|
||||
}
|
||||
|
||||
type paddingLengthRange2612 struct {
|
||||
minimum int
|
||||
maximum int
|
||||
}
|
||||
|
||||
type serverPlayLengthBranches2612 struct {
|
||||
small paddingLengthRange2612
|
||||
large paddingLengthRange2612
|
||||
}
|
||||
|
||||
var serverPlayBranches2612 = []serverPlayLengthBranches2612{
|
||||
{small: paddingLengthRange2612{346, 18812}, large: paddingLengthRange2612{51702, 58638}},
|
||||
{small: paddingLengthRange2612{388, 20689}, large: paddingLengthRange2612{51445, 61077}},
|
||||
{small: paddingLengthRange2612{575, 20915}, large: paddingLengthRange2612{41428, 65584}},
|
||||
{small: paddingLengthRange2612{86, 2772}, large: paddingLengthRange2612{41428, 63563}},
|
||||
{small: paddingLengthRange2612{42, 26813}, large: paddingLengthRange2612{51983, 51983}},
|
||||
{small: paddingLengthRange2612{309, 19484}, large: paddingLengthRange2612{24837, 25083}},
|
||||
{small: paddingLengthRange2612{74, 40686}, large: paddingLengthRange2612{63885, 63885}},
|
||||
{small: paddingLengthRange2612{30, 44114}, large: paddingLengthRange2612{66128, 66128}},
|
||||
{small: paddingLengthRange2612{26, 1464}, large: paddingLengthRange2612{9941, 35818}},
|
||||
{small: paddingLengthRange2612{35, 42885}, large: paddingLengthRange2612{52194, 59407}},
|
||||
{small: paddingLengthRange2612{37, 47553}, large: paddingLengthRange2612{61765, 65328}},
|
||||
{small: paddingLengthRange2612{26, 1121}, large: paddingLengthRange2612{16162, 60622}},
|
||||
{small: paddingLengthRange2612{11, 45629}, large: paddingLengthRange2612{60808, 60808}},
|
||||
{small: paddingLengthRange2612{55, 10035}, large: paddingLengthRange2612{30237, 62027}},
|
||||
{small: paddingLengthRange2612{427, 52536}, large: paddingLengthRange2612{64014, 65622}},
|
||||
{small: paddingLengthRange2612{35, 22708}, large: paddingLengthRange2612{38987, 59401}},
|
||||
}
|
||||
|
||||
// Each mask preserves only the small/large branch order from one baseline
|
||||
// login. Actual lengths and timing are selected randomly inside each branch.
|
||||
var serverPlayBranchMasks2612 = []uint32{
|
||||
0x011c, 0x090a, 0x0821, 0xe921, 0x2102,
|
||||
0x0844, 0xa101, 0x1106, 0x2e00, 0xab01,
|
||||
0xe900, 0xac01, 0xab01, 0x8b80, 0x0808,
|
||||
0x2001, 0x0901, 0x000a, 0x2c01, 0x0801,
|
||||
}
|
||||
|
||||
type clientPlayBurst2612 struct {
|
||||
playIndex int
|
||||
regular paddingLengthRange2612
|
||||
burst paddingLengthRange2612
|
||||
}
|
||||
|
||||
var clientPlayBursts2612 = []clientPlayBurst2612{
|
||||
{playIndex: 0, regular: paddingLengthRange2612{6, 44}, burst: paddingLengthRange2612{877, 883}},
|
||||
{playIndex: 2, regular: paddingLengthRange2612{6, 45}, burst: paddingLengthRange2612{884, 887}},
|
||||
{playIndex: 10, regular: paddingLengthRange2612{2, 19}, burst: paddingLengthRange2612{851, 851}},
|
||||
}
|
||||
|
||||
// The 20 samples placed the one client initialization burst in these slots.
|
||||
var clientPlayBurstChoices2612 = []int{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
1, 1,
|
||||
2,
|
||||
}
|
||||
|
||||
var paddingSchedule2612 = buildPaddingSchedule2612()
|
||||
|
||||
func buildPaddingSchedule2612() []paddingTurn {
|
||||
schedule := make([]paddingTurn, 0, len(startupPaddingSchedule2612)+len(playJoinPaddingSchedule2612))
|
||||
schedule = append(schedule, startupPaddingSchedule2612...)
|
||||
schedule = append(schedule, playJoinPaddingSchedule2612...)
|
||||
return schedule
|
||||
}
|
||||
|
||||
func clientPlayPaddingTurn(minimum, maximum int) paddingTurn {
|
||||
return paddingTurn{
|
||||
direction: paddingClientToServer,
|
||||
minLength: minimum,
|
||||
maxLength: maximum,
|
||||
startDelay: millisecondRange(1, 30),
|
||||
writeChunkLength: 1024,
|
||||
}
|
||||
}
|
||||
|
||||
func serverPlayPaddingTurn(minimum, maximum int) paddingTurn {
|
||||
return paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: minimum,
|
||||
maxLength: maximum,
|
||||
startDelay: millisecondRange(1, 45),
|
||||
chunkDelay: millisecondRange(1, 4),
|
||||
writeChunkMinLength: 32 * 1024,
|
||||
writeChunkLength: maxPaddingChunkLength,
|
||||
}
|
||||
}
|
||||
|
||||
func newClientPaddingSchedule2612() ([]paddingTurn, error) {
|
||||
choice, err := randomPaddingIndex(len(clientPlayBurstChoices2612))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectedBurst := clientPlayBurstChoices2612[choice]
|
||||
schedule := append([]paddingTurn(nil), paddingSchedule2612...)
|
||||
for i, burst := range clientPlayBursts2612 {
|
||||
lengthRange := burst.regular
|
||||
if i == selectedBurst {
|
||||
lengthRange = burst.burst
|
||||
}
|
||||
turn := &schedule[len(startupPaddingSchedule2612)+burst.playIndex]
|
||||
turn.sendMinLength = lengthRange.minimum
|
||||
turn.sendMaxLength = lengthRange.maximum
|
||||
}
|
||||
return schedule, nil
|
||||
}
|
||||
|
||||
type paddingPause struct {
|
||||
chunk int
|
||||
delay paddingDelayRange
|
||||
}
|
||||
|
||||
func paddingVariantFromChunks(chunks ...int) paddingVariant {
|
||||
return paddingVariant{chunks: chunks}
|
||||
}
|
||||
|
||||
func pacedPaddingVariant(chunks []int, pauses ...paddingPause) paddingVariant {
|
||||
delays := make([]paddingDelayRange, len(chunks))
|
||||
for _, pause := range pauses {
|
||||
if pause.chunk < 0 || pause.chunk >= len(delays) {
|
||||
panic("xmc: padding pause index is outside its chunk template")
|
||||
}
|
||||
delays[pause.chunk] = pause.delay
|
||||
}
|
||||
return paddingVariant{chunks: chunks, delays: delays}
|
||||
}
|
||||
|
||||
func registryPaddingVariant() paddingVariant {
|
||||
return pacedPaddingVariant(
|
||||
[]int{1590, 226, 329, 229, 186, 151, 78, 81, 79, 235, 67, 67, 78, 71, 82, 74, 982, 117, 1118, 1038, 970, 400, 239, 49, 50, 95, 65, 104, 32320, 2},
|
||||
paddingPause{28, millisecondRange(1, 4)},
|
||||
paddingPause{29, millisecondRange(44, 61)},
|
||||
)
|
||||
}
|
||||
|
||||
func playStartPaddingVariant(chunks ...int) paddingVariant {
|
||||
if len(chunks) < 2 {
|
||||
panic("xmc: play start padding variant needs at least two chunks")
|
||||
}
|
||||
return pacedPaddingVariant(
|
||||
chunks,
|
||||
paddingPause{len(chunks) / 2, millisecondRange(1, 5)},
|
||||
paddingPause{len(chunks) - 1, millisecondRange(9, 20)},
|
||||
)
|
||||
}
|
||||
|
||||
func millisecondRange(minimum, maximum int) paddingDelayRange {
|
||||
return paddingDelayRange{
|
||||
min: time.Duration(minimum) * time.Millisecond,
|
||||
max: time.Duration(maximum) * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
func newServerPaddingSchedule2612() ([]paddingTurn, error) {
|
||||
schedule := append([]paddingTurn(nil), paddingSchedule2612...)
|
||||
profileIndex, err := randomPaddingIndex(len(serverPlayBranchMasks2612))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile := serverPlayBranchMasks2612[profileIndex]
|
||||
for i, branches := range serverPlayBranches2612 {
|
||||
lengthRange := branches.small
|
||||
if profile&(1<<i) != 0 {
|
||||
lengthRange = branches.large
|
||||
}
|
||||
turn := &schedule[len(startupPaddingSchedule2612)+1+i*2]
|
||||
turn.sendMinLength = lengthRange.minimum
|
||||
turn.sendMaxLength = lengthRange.maximum
|
||||
}
|
||||
return schedule, nil
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPaddingTurnReachesFinalTargetLength(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 128, maxLength: 128}
|
||||
const prefixLength = 3
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, prefixLength); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := prefixLength + encoded.Len(); got != turn.minLength {
|
||||
t.Fatalf("total turn length = %d, want %d", got, turn.minLength)
|
||||
}
|
||||
encodedReader := bytes.NewReader(encoded.Bytes())
|
||||
var recordLength Varint
|
||||
if err := recordLength.readFrom(encodedReader); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := int(recordLength); got != encoded.Len() {
|
||||
t.Fatalf("record length = %d, encoded = %d", got, encoded.Len())
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, prefixLength); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnSupportsThreeByteTarget(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 3, maxLength: 3}
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := encoded.Len(); got != 3 {
|
||||
t.Fatalf("padding length = %d, want 3", got)
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnVarintBoundaries(t *testing.T) {
|
||||
for _, targetLength := range []int{127, 128, 16383, 16384} {
|
||||
t.Run(strconv.Itoa(targetLength), func(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: targetLength, maxLength: targetLength}
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encoded.Len() != targetLength {
|
||||
t.Fatalf("padding length = %d, want %d", encoded.Len(), targetLength)
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnRandomRange(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingServerToClient, minLength: 127, maxLength: 129}
|
||||
seen := make(map[int]bool)
|
||||
for range 100 {
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encoded.Len() < turn.minLength || encoded.Len() > turn.maxLength {
|
||||
t.Fatalf("padding length = %d", encoded.Len())
|
||||
}
|
||||
seen[encoded.Len()] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("padding range did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingTurnUsesRestrictedSendRange(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: 3,
|
||||
maxLength: 100,
|
||||
sendMinLength: 90,
|
||||
sendMaxLength: 100,
|
||||
}
|
||||
seen := make(map[int]bool)
|
||||
for range 100 {
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if encoded.Len() < turn.sendMinLength || encoded.Len() > turn.sendMaxLength {
|
||||
t.Fatalf("padding length = %d", encoded.Len())
|
||||
}
|
||||
seen[encoded.Len()] = true
|
||||
if err := readPaddingTurn(bytes.NewReader(encoded.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("restricted send range did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingScheduleSynchronizesDirections(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
schedule := []paddingTurn{
|
||||
{direction: paddingClientToServer, minLength: 33, maxLength: 33},
|
||||
{direction: paddingServerToClient, minLength: 4097, maxLength: 4097},
|
||||
{direction: paddingClientToServer, minLength: 16385, maxLength: 16385},
|
||||
}
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
serverDone <- runPaddingSchedule(server, server, false, 3, schedule)
|
||||
}()
|
||||
if err := runPaddingSchedule(client, client, true, 3, schedule); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err := <-serverDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("server padding schedule did not complete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnHandlesFragmentedInput(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 1024, maxLength: 1024}
|
||||
var encoded bytes.Buffer
|
||||
if err := writePaddingTurn(&encoded, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := readPaddingTurn(&oneByteReader{reader: bytes.NewReader(encoded.Bytes())}, turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnRejectsInvalidLength(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 96}
|
||||
data := encodePaddingLength(t, 63)
|
||||
if err := readPaddingTurn(bytes.NewReader(data), turn, 0); err == nil || !strings.Contains(err.Error(), "outside") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnRejectsNonCanonicalHeader(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 3, maxLength: 3}
|
||||
err := readPaddingTurn(bytes.NewReader([]byte{0x83, 0x00, 0x00}), turn, 0)
|
||||
if err == nil || !strings.Contains(err.Error(), "non-canonical") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnRejectsTruncatedBody(t *testing.T) {
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 64}
|
||||
data := encodePaddingLength(t, 64)
|
||||
if err := readPaddingTurn(bytes.NewReader(data), turn, 0); err == nil || !strings.Contains(err.Error(), "body") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPaddingTurnHonorsConnectionTimeout(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
|
||||
if err := server.SetReadDeadline(time.Now().Add(20 * time.Millisecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
turn := paddingTurn{direction: paddingClientToServer, minLength: 64, maxLength: 64}
|
||||
err := readPaddingTurn(server, turn, 0)
|
||||
var netErr net.Error
|
||||
if !errors.As(err, &netErr) || !netErr.Timeout() {
|
||||
t.Fatalf("error = %v, want network timeout", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePaddingSchedule(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schedule []paddingTurn
|
||||
prefix int
|
||||
}{
|
||||
{name: "empty"},
|
||||
{name: "bad direction", schedule: []paddingTurn{{direction: 99, minLength: 4, maxLength: 4}}},
|
||||
{name: "too small", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 0, maxLength: 4}}},
|
||||
{name: "reversed range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 7}}},
|
||||
{name: "wrong first direction", prefix: 3, schedule: []paddingTurn{{direction: paddingServerToClient, minLength: 8, maxLength: 8}}},
|
||||
{name: "prefix leaves no header", prefix: 8, schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8}}},
|
||||
{name: "same direction", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8}, {direction: paddingClientToServer, minLength: 8, maxLength: 8}}},
|
||||
{name: "range with variants", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, variants: []paddingVariant{paddingVariantFromChunks(8)}}}},
|
||||
{name: "empty variant", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{{}}}}},
|
||||
{name: "bad chunk", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(maxPaddingChunkLength + 1)}}}},
|
||||
{
|
||||
name: "delay mismatch",
|
||||
schedule: []paddingTurn{{
|
||||
direction: paddingClientToServer,
|
||||
variants: []paddingVariant{{
|
||||
chunks: []int{4, 4},
|
||||
delays: []paddingDelayRange{{min: time.Millisecond, max: time.Millisecond}},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
{name: "reversed start delay", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, startDelay: paddingDelayRange{min: 2 * time.Millisecond, max: time.Millisecond}}}},
|
||||
{name: "reversed generated chunk delay", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, chunkDelay: paddingDelayRange{min: 2 * time.Millisecond, max: time.Millisecond}}}},
|
||||
{name: "oversized generated chunk", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, writeChunkLength: maxPaddingChunkLength + 1}}},
|
||||
{name: "reversed generated chunk range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 8, writeChunkMinLength: 9, writeChunkLength: 8}}},
|
||||
{name: "variant with generated chunks", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, writeChunkLength: 8}}},
|
||||
{name: "send range outside accepted range", schedule: []paddingTurn{{direction: paddingClientToServer, minLength: 8, maxLength: 16, sendMinLength: 7, sendMaxLength: 12}}},
|
||||
{name: "variant with send range", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, sendMinLength: 8, sendMaxLength: 8}}},
|
||||
{name: "negative chunk delay", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{{chunks: []int{8}, delays: []paddingDelayRange{{min: -time.Millisecond}}}}}}},
|
||||
{name: "bad send variant", schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8)}, sendVariants: []int{1}}}},
|
||||
{name: "prefix splits chunk", prefix: 3, schedule: []paddingTurn{{direction: paddingClientToServer, variants: []paddingVariant{paddingVariantFromChunks(8, 4)}}}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if err := validatePaddingSchedule(test.schedule, test.prefix); err == nil {
|
||||
t.Fatal("expected invalid padding schedule")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingSchedule2612MatchesCapturedTemplates(t *testing.T) {
|
||||
wantDirections := []paddingDirection{
|
||||
paddingClientToServer,
|
||||
paddingServerToClient,
|
||||
paddingClientToServer,
|
||||
paddingServerToClient,
|
||||
paddingClientToServer,
|
||||
paddingServerToClient,
|
||||
}
|
||||
wantLengths := [][]int{
|
||||
{44},
|
||||
{72},
|
||||
{25},
|
||||
{41172},
|
||||
{2},
|
||||
{7464, 8267, 8790, 8153, 7375, 7347, 8184, 6633, 7670, 8241, 7857, 7254, 8407, 8283, 6804, 8177, 8177, 7929, 8296, 8177},
|
||||
}
|
||||
if len(paddingSchedule2612) != len(wantDirections)+33 {
|
||||
t.Fatalf("padding schedule has %d turns, want %d", len(paddingSchedule2612), len(wantDirections)+33)
|
||||
}
|
||||
for i, turn := range paddingSchedule2612[:len(wantDirections)] {
|
||||
if turn.direction != wantDirections[i] {
|
||||
t.Fatalf("padding turn %d direction = %d, want %d", i, turn.direction, wantDirections[i])
|
||||
}
|
||||
if len(turn.variants) != len(wantLengths[i]) {
|
||||
t.Fatalf("padding turn %d has %d variants, want %d", i, len(turn.variants), len(wantLengths[i]))
|
||||
}
|
||||
for j, variant := range turn.variants {
|
||||
if got := paddingVariantLength(variant); got != wantLengths[i][j] {
|
||||
t.Fatalf("padding turn %d variant %d length = %d, want %d", i, j, got, wantLengths[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
wantPlayBounds := [][2]int{
|
||||
{6, 883},
|
||||
{346, 58638},
|
||||
{6, 887},
|
||||
{388, 61077},
|
||||
{2, 50},
|
||||
{575, 65584},
|
||||
{6, 45},
|
||||
{86, 63563},
|
||||
{2, 44},
|
||||
{42, 51983},
|
||||
{2, 851},
|
||||
{309, 25083},
|
||||
{2, 19},
|
||||
{74, 63885},
|
||||
{8, 24},
|
||||
{30, 66128},
|
||||
{2, 19},
|
||||
{26, 35818},
|
||||
{6, 19},
|
||||
{35, 59407},
|
||||
{6, 19},
|
||||
{37, 65328},
|
||||
{2, 19},
|
||||
{26, 60622},
|
||||
{6, 19},
|
||||
{11, 60808},
|
||||
{8, 43},
|
||||
{55, 62027},
|
||||
{2, 19},
|
||||
{427, 65622},
|
||||
{5, 19},
|
||||
{35, 59401},
|
||||
{6, 19},
|
||||
}
|
||||
for i, want := range wantPlayBounds {
|
||||
turn := paddingSchedule2612[len(wantDirections)+i]
|
||||
wantDirection := paddingClientToServer
|
||||
if i%2 == 1 {
|
||||
wantDirection = paddingServerToClient
|
||||
}
|
||||
if turn.direction != wantDirection {
|
||||
t.Fatalf("play turn %d direction = %d, want %d", i, turn.direction, wantDirection)
|
||||
}
|
||||
if turn.minLength != want[0] || turn.maxLength != want[1] {
|
||||
t.Fatalf("play turn %d bounds = %d-%d, want %d-%d", i, turn.minLength, turn.maxLength, want[0], want[1])
|
||||
}
|
||||
if len(turn.variants) != 0 {
|
||||
t.Fatalf("play turn %d unexpectedly has captured variants", i)
|
||||
}
|
||||
}
|
||||
if got := len(paddingSchedule2612[3].variants[0].chunks); got != 30 {
|
||||
t.Fatalf("registry turn chunks = %d, want 30", got)
|
||||
}
|
||||
minimumPlayStart := maxPaddingTurnLength
|
||||
maximumPlayStart := 0
|
||||
for _, variant := range paddingSchedule2612[5].variants {
|
||||
length := paddingVariantLength(variant)
|
||||
minimumPlayStart = min(minimumPlayStart, length)
|
||||
maximumPlayStart = max(maximumPlayStart, length)
|
||||
if variant.chunks[0] != 4941 {
|
||||
t.Fatalf("play start first chunk = %d, want 4941", variant.chunks[0])
|
||||
}
|
||||
}
|
||||
if minimumPlayStart != 6633 || maximumPlayStart != 8790 {
|
||||
t.Fatalf("play start bounds = %d-%d, want 6633-8790", minimumPlayStart, maximumPlayStart)
|
||||
}
|
||||
if err := validatePaddingSchedule(paddingSchedule2612, 2); err != nil {
|
||||
t.Fatalf("captured schedule is invalid: %v", err)
|
||||
}
|
||||
|
||||
serverSchedule, err := newServerPaddingSchedule2612()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = validatePaddingSchedule(serverSchedule, 2); err != nil {
|
||||
t.Fatalf("server schedule is invalid: %v", err)
|
||||
}
|
||||
for i, branches := range serverPlayBranches2612 {
|
||||
turn := serverSchedule[len(startupPaddingSchedule2612)+1+i*2]
|
||||
got := paddingLengthRange2612{turn.sendMinLength, turn.sendMaxLength}
|
||||
if got != branches.small && got != branches.large {
|
||||
t.Fatalf("server play turn %d send range = %v, want %v or %v", i, got, branches.small, branches.large)
|
||||
}
|
||||
}
|
||||
|
||||
for range 20 {
|
||||
clientSchedule, clientErr := newClientPaddingSchedule2612()
|
||||
if clientErr != nil {
|
||||
t.Fatal(clientErr)
|
||||
}
|
||||
if clientErr = validatePaddingSchedule(clientSchedule, 2); clientErr != nil {
|
||||
t.Fatalf("client schedule is invalid: %v", clientErr)
|
||||
}
|
||||
burstCount := 0
|
||||
for _, burst := range clientPlayBursts2612 {
|
||||
turn := clientSchedule[len(startupPaddingSchedule2612)+burst.playIndex]
|
||||
got := paddingLengthRange2612{turn.sendMinLength, turn.sendMaxLength}
|
||||
switch got {
|
||||
case burst.regular:
|
||||
case burst.burst:
|
||||
burstCount++
|
||||
default:
|
||||
t.Fatalf("client play turn %d send range = %v", burst.playIndex, got)
|
||||
}
|
||||
}
|
||||
if burstCount != 1 {
|
||||
t.Fatalf("client schedule has %d initialization bursts, want 1", burstCount)
|
||||
}
|
||||
}
|
||||
|
||||
for variantIndex := range paddingSchedule2612[3].variants {
|
||||
turn := paddingSchedule2612[3]
|
||||
turn.sendVariants = []int{variantIndex}
|
||||
var encoded bytes.Buffer
|
||||
if err = writePaddingTurnWithSleep(&encoded, turn, 0, func(time.Duration) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = readPaddingTurn(bytes.NewReader(encoded.Bytes()), paddingSchedule2612[3], 0); err != nil {
|
||||
t.Fatalf("registry variant %d was rejected: %v", variantIndex, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingVariantPreservesWriteBoundaries(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
turn paddingTurn
|
||||
prefix int
|
||||
want []int
|
||||
}{
|
||||
{name: "login acknowledged turn", turn: paddingSchedule2612[0], prefix: 2, want: []int{26, 16}},
|
||||
{name: "server response turn", turn: paddingSchedule2612[1], want: []int{26, 21, 25}},
|
||||
{name: "single packet turn", turn: paddingSchedule2612[2], want: []int{25}},
|
||||
{name: "fixed registry profile", turn: paddingSchedule2612[3], want: paddingSchedule2612[3].variants[0].chunks},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, test.turn, test.prefix, func(time.Duration) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(writer.writes) != len(test.want) {
|
||||
t.Fatalf("writes = %v, want %v", writer.writes, test.want)
|
||||
}
|
||||
for i := range test.want {
|
||||
if writer.writes[i] != test.want[i] {
|
||||
t.Fatalf("writes = %v, want %v", writer.writes, test.want)
|
||||
}
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(writer.Bytes()), test.turn, test.prefix); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingVariantAppliesPacing(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingClientToServer,
|
||||
startDelay: paddingDelayRange{min: 3 * time.Millisecond, max: 3 * time.Millisecond},
|
||||
variants: []paddingVariant{{
|
||||
chunks: []int{3, 5, 7},
|
||||
delays: []paddingDelayRange{
|
||||
{},
|
||||
{min: 2 * time.Millisecond, max: 2 * time.Millisecond},
|
||||
{min: 4 * time.Millisecond, max: 4 * time.Millisecond},
|
||||
},
|
||||
}},
|
||||
}
|
||||
var slept []time.Duration
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, turn, 3, func(delay time.Duration) {
|
||||
slept = append(slept, delay)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []time.Duration{3 * time.Millisecond, 2 * time.Millisecond, 4 * time.Millisecond}
|
||||
if len(slept) != len(want) {
|
||||
t.Fatalf("delays = %v, want %v", slept, want)
|
||||
}
|
||||
for i := range want {
|
||||
if slept[i] != want[i] {
|
||||
t.Fatalf("delays = %v, want %v", slept, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedPaddingChunksApplyPacing(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: 100,
|
||||
maxLength: 100,
|
||||
writeChunkLength: 32,
|
||||
chunkDelay: paddingDelayRange{min: 2 * time.Millisecond, max: 2 * time.Millisecond},
|
||||
}
|
||||
var slept []time.Duration
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, turn, 0, func(delay time.Duration) {
|
||||
slept = append(slept, delay)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantWrites := []int{32, 32, 32, 4}
|
||||
if !slicesEqual(writer.writes, wantWrites) {
|
||||
t.Fatalf("writes = %v, want %v", writer.writes, wantWrites)
|
||||
}
|
||||
wantSleeps := []time.Duration{2 * time.Millisecond, 2 * time.Millisecond, 2 * time.Millisecond}
|
||||
if !slicesEqual(slept, wantSleeps) {
|
||||
t.Fatalf("delays = %v, want %v", slept, wantSleeps)
|
||||
}
|
||||
if err := readPaddingTurn(bytes.NewReader(writer.Bytes()), turn, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedPaddingChunkLengthIsRandomized(t *testing.T) {
|
||||
turn := paddingTurn{
|
||||
direction: paddingServerToClient,
|
||||
minLength: 100,
|
||||
maxLength: 100,
|
||||
writeChunkMinLength: 16,
|
||||
writeChunkLength: 32,
|
||||
}
|
||||
seen := make(map[int]bool)
|
||||
for range 100 {
|
||||
var writer recordingWriter
|
||||
if err := writePaddingTurnWithSleep(&writer, turn, 0, func(time.Duration) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
firstWrite := writer.writes[0]
|
||||
if firstWrite < turn.writeChunkMinLength || firstWrite > turn.writeChunkLength {
|
||||
t.Fatalf("first write = %d", firstWrite)
|
||||
}
|
||||
seen[firstWrite] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("generated write chunk length did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingDelayRangeIsRandomized(t *testing.T) {
|
||||
delayRange := millisecondRange(25, 40)
|
||||
seen := make(map[time.Duration]bool)
|
||||
for range 100 {
|
||||
delay, err := randomPaddingDelay(delayRange)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if delay < delayRange.min || delay > delayRange.max {
|
||||
t.Fatalf("delay = %s, want %s-%s", delay, delayRange.min, delayRange.max)
|
||||
}
|
||||
seen[delay] = true
|
||||
}
|
||||
if len(seen) < 2 {
|
||||
t.Fatalf("padding delay did not vary: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddingSchedule2612UsesCoarseTimingBands(t *testing.T) {
|
||||
assertDelayRange(t, "turn 3 to 4", paddingSchedule2612[3].startDelay, 20*time.Millisecond, 50*time.Millisecond)
|
||||
assertDelayRange(t, "turn 5 to 6", paddingSchedule2612[5].startDelay, 35*time.Millisecond, 50*time.Millisecond)
|
||||
assertDelayRange(t, "first play client turn", paddingSchedule2612[6].startDelay, time.Millisecond, 30*time.Millisecond)
|
||||
assertDelayRange(t, "first play server turn", paddingSchedule2612[7].startDelay, time.Millisecond, 45*time.Millisecond)
|
||||
assertDelayRange(t, "play server chunk pacing", paddingSchedule2612[7].chunkDelay, time.Millisecond, 4*time.Millisecond)
|
||||
if paddingSchedule2612[6].writeChunkLength != 1024 {
|
||||
t.Fatalf("play client write chunk = %d, want 1024", paddingSchedule2612[6].writeChunkLength)
|
||||
}
|
||||
if paddingSchedule2612[7].writeChunkLength != maxPaddingChunkLength {
|
||||
t.Fatalf("play server write chunk = %d, want %d", paddingSchedule2612[7].writeChunkLength, maxPaddingChunkLength)
|
||||
}
|
||||
if paddingSchedule2612[7].writeChunkMinLength != 32*1024 {
|
||||
t.Fatalf("play server minimum write chunk = %d, want %d", paddingSchedule2612[7].writeChunkMinLength, 32*1024)
|
||||
}
|
||||
|
||||
for i, variant := range paddingSchedule2612[3].variants {
|
||||
minimum, maximum := paddingVariantDelayBounds(variant)
|
||||
if minimum != 45*time.Millisecond || maximum != 65*time.Millisecond {
|
||||
t.Fatalf("turn 4 variant %d duration = %s-%s, want 45ms-65ms", i, minimum, maximum)
|
||||
}
|
||||
}
|
||||
for i, variant := range paddingSchedule2612[5].variants {
|
||||
minimum, maximum := paddingVariantDelayBounds(variant)
|
||||
if minimum != 10*time.Millisecond || maximum != 25*time.Millisecond {
|
||||
t.Fatalf("turn 6 variant %d duration = %s-%s, want 10ms-25ms", i, minimum, maximum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertDelayRange(t *testing.T, name string, got paddingDelayRange, minimum, maximum time.Duration) {
|
||||
t.Helper()
|
||||
if got.min != minimum || got.max != maximum {
|
||||
t.Fatalf("%s delay = %s-%s, want %s-%s", name, got.min, got.max, minimum, maximum)
|
||||
}
|
||||
}
|
||||
|
||||
func paddingVariantDelayBounds(variant paddingVariant) (time.Duration, time.Duration) {
|
||||
var minimum time.Duration
|
||||
var maximum time.Duration
|
||||
for _, delay := range variant.delays {
|
||||
minimum += delay.min
|
||||
maximum += delay.max
|
||||
}
|
||||
return minimum, maximum
|
||||
}
|
||||
|
||||
func slicesEqual[T comparable](left, right []T) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for i := range left {
|
||||
if left[i] != right[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func encodePaddingLength(t *testing.T, length int) []byte {
|
||||
t.Helper()
|
||||
var encoded bytes.Buffer
|
||||
value := Varint(length)
|
||||
if err := value.writeTo(&encoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return encoded.Bytes()
|
||||
}
|
||||
|
||||
type oneByteReader struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func (r *oneByteReader) Read(p []byte) (int, error) {
|
||||
if len(p) > 1 {
|
||||
p = p[:1]
|
||||
}
|
||||
return r.reader.Read(p)
|
||||
}
|
||||
|
||||
type recordingWriter struct {
|
||||
bytes.Buffer
|
||||
writes []int
|
||||
}
|
||||
|
||||
func (w *recordingWriter) Write(p []byte) (int, error) {
|
||||
w.writes = append(w.writes, len(p))
|
||||
return w.Buffer.Write(p)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package xmc
|
||||
|
||||
import "fmt"
|
||||
|
||||
type loginProfile struct {
|
||||
Username string
|
||||
UUID UUID
|
||||
TexturesValue string
|
||||
TexturesSignature string
|
||||
}
|
||||
|
||||
func profilesFromConfig(configured []*Profile) ([]loginProfile, error) {
|
||||
if len(configured) == 0 {
|
||||
return nil, fmt.Errorf("empty profiles")
|
||||
}
|
||||
|
||||
profiles := make([]loginProfile, 0, len(configured))
|
||||
for _, configuredProfile := range configured {
|
||||
if configuredProfile == nil || configuredProfile.Username == "" {
|
||||
return nil, fmt.Errorf("invalid profile")
|
||||
}
|
||||
if len(configuredProfile.Uuid) != len(UUID{}) {
|
||||
return nil, fmt.Errorf("bad profile UUID length: %d", len(configuredProfile.Uuid))
|
||||
}
|
||||
if configuredProfile.TexturesValue == "" || configuredProfile.TexturesSignature == "" {
|
||||
return nil, fmt.Errorf("incomplete profile textures")
|
||||
}
|
||||
|
||||
profile := loginProfile{
|
||||
Username: configuredProfile.Username,
|
||||
TexturesValue: configuredProfile.TexturesValue,
|
||||
TexturesSignature: configuredProfile.TexturesSignature,
|
||||
}
|
||||
copy(profile.UUID[:], configuredProfile.Uuid)
|
||||
profiles = append(profiles, profile)
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func findProfile(profiles []loginProfile, username string, uuid UUID) (loginProfile, bool) {
|
||||
for _, profile := range profiles {
|
||||
if profile.Username == username && profile.UUID == uuid {
|
||||
return profile, true
|
||||
}
|
||||
}
|
||||
return loginProfile{}, false
|
||||
}
|
||||
|
||||
func readLoginSuccess(packet *mcPacket) (loginProfile, error) {
|
||||
var (
|
||||
profile loginProfile
|
||||
username String
|
||||
propertyCount Varint
|
||||
propertyName String
|
||||
value String
|
||||
signed Boolean
|
||||
signature String
|
||||
)
|
||||
if err := packet.readFields(&profile.UUID, &username, &propertyCount, &propertyName, &value, &signed, &signature); err != nil {
|
||||
return loginProfile{}, err
|
||||
}
|
||||
if propertyCount != 1 || propertyName != "textures" || !signed {
|
||||
return loginProfile{}, fmt.Errorf("invalid login profile properties")
|
||||
}
|
||||
profile.Username = string(username)
|
||||
profile.TexturesValue = string(value)
|
||||
profile.TexturesSignature = string(signature)
|
||||
return profile, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProfilesFromConfigRejectsEmpty(t *testing.T) {
|
||||
if _, err := profilesFromConfig(nil); err == nil {
|
||||
t.Fatal("expected empty profiles error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfilesFromConfig(t *testing.T) {
|
||||
uuid := bytes.Repeat([]byte{0x2a}, 16)
|
||||
profiles, err := profilesFromConfig([]*Profile{
|
||||
{
|
||||
Username: "SignedUser",
|
||||
Uuid: uuid,
|
||||
TexturesValue: "textures-value",
|
||||
TexturesSignature: "textures-signature",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("build explicit profile: %v", err)
|
||||
}
|
||||
if len(profiles) != 1 || profiles[0].Username != "SignedUser" {
|
||||
t.Fatalf("unexpected profile: %+v", profiles)
|
||||
}
|
||||
if profiles[0].TexturesValue != "textures-value" || profiles[0].TexturesSignature != "textures-signature" {
|
||||
t.Fatalf("textures were not preserved: %+v", profiles[0])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
// Minecraft protocol
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
maxPacketDataLength = 32 * 1024
|
||||
maxPacketBodyLength = maxPacketDataLength + 5
|
||||
)
|
||||
|
||||
type field interface {
|
||||
readFrom(r io.Reader) error
|
||||
writeTo(w io.Writer) error
|
||||
}
|
||||
|
||||
type mcPacket struct {
|
||||
packetID int
|
||||
data []byte
|
||||
}
|
||||
|
||||
func readPacket(b io.Reader) (*mcPacket, error) {
|
||||
packet, _, err := readPacketWithLength(b)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
func readPacketWithLength(b io.Reader) (*mcPacket, int, error) {
|
||||
packetData, wireLength, err := readFrame(b, maxPacketBodyLength)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
packet, err := decodePacketBody(packetData)
|
||||
return packet, wireLength, err
|
||||
}
|
||||
|
||||
func decodePacketBody(packetData []byte) (*mcPacket, error) {
|
||||
if len(packetData) < 1 || len(packetData) > maxPacketBodyLength {
|
||||
return nil, fmt.Errorf("read packet: bad length: %d", len(packetData))
|
||||
}
|
||||
|
||||
body := bytes.NewReader(packetData)
|
||||
var packetID Varint
|
||||
err := packetID.readFrom(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read packet ID: %w", err)
|
||||
}
|
||||
|
||||
dataLength := body.Len()
|
||||
if dataLength > maxPacketDataLength {
|
||||
return nil, fmt.Errorf("read packet: bad length: %d", dataLength)
|
||||
}
|
||||
|
||||
data := make([]byte, dataLength)
|
||||
_, err = io.ReadFull(body, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read packet data: %w", err)
|
||||
}
|
||||
|
||||
return &mcPacket{
|
||||
packetID: int(packetID),
|
||||
data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readFrame(r io.Reader, maxLength int) ([]byte, int, error) {
|
||||
frameLength, prefixLength, err := readVarintWithLength(r)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("read packet length: %w", err)
|
||||
}
|
||||
if frameLength < 1 || int(frameLength) > maxLength {
|
||||
return nil, 0, fmt.Errorf("read packet: bad length: %d", frameLength)
|
||||
}
|
||||
|
||||
frame := make([]byte, int(frameLength))
|
||||
if _, err := io.ReadFull(r, frame); err != nil {
|
||||
return nil, 0, fmt.Errorf("read packet data: %w", err)
|
||||
}
|
||||
return frame, prefixLength + len(frame), nil
|
||||
}
|
||||
|
||||
func (p *mcPacket) readFields(fields ...field) error {
|
||||
r := bytes.NewReader(p.data)
|
||||
for _, field := range fields {
|
||||
err := field.readFrom(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read packet field: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Varint int32
|
||||
|
||||
const (
|
||||
SEGMENT_BITS = 0x7F
|
||||
CONTINUE_BIT = 0x80
|
||||
)
|
||||
|
||||
func (v *Varint) readFrom(r io.Reader) error {
|
||||
value, _, err := readVarintWithLength(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*v = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func readVarintWithLength(r io.Reader) (Varint, int, error) {
|
||||
var value int32
|
||||
for index := 0; index < 5; index++ {
|
||||
currentByte, err := readByte(r)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("read varint: %w", err)
|
||||
}
|
||||
if index == 4 && currentByte&0xf0 != 0 {
|
||||
return 0, 0, fmt.Errorf("read varint: too large")
|
||||
}
|
||||
value |= int32(currentByte&SEGMENT_BITS) << (7 * index)
|
||||
|
||||
if currentByte&CONTINUE_BIT == 0 {
|
||||
parsed := Varint(value)
|
||||
length := index + 1
|
||||
if length != varintSize(parsed) {
|
||||
return 0, 0, fmt.Errorf("read varint: non-canonical encoding")
|
||||
}
|
||||
return parsed, length, nil
|
||||
}
|
||||
}
|
||||
return 0, 0, fmt.Errorf("read varint: too large")
|
||||
}
|
||||
|
||||
func (v *Varint) writeTo(w io.Writer) error {
|
||||
value := uint32(*v)
|
||||
|
||||
for {
|
||||
currentByte := byte(value & SEGMENT_BITS)
|
||||
value >>= 7
|
||||
if value != 0 {
|
||||
currentByte |= CONTINUE_BIT
|
||||
}
|
||||
|
||||
_, err := w.Write([]byte{currentByte})
|
||||
if err != nil {
|
||||
return fmt.Errorf("write varint: %w", err)
|
||||
}
|
||||
|
||||
if value == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func varintSize(value Varint) int {
|
||||
uintValue := uint32(value)
|
||||
size := 0
|
||||
for range 5 {
|
||||
size++
|
||||
uintValue >>= 7
|
||||
if uintValue == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
type String string
|
||||
|
||||
func (v *String) readFrom(r io.Reader) error {
|
||||
var length Varint = 0
|
||||
|
||||
err := length.readFrom(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read string: %w", err)
|
||||
}
|
||||
|
||||
if length < 0 || length > 4096 {
|
||||
return fmt.Errorf("read string: bad length: %d", length)
|
||||
}
|
||||
|
||||
buf := make([]byte, length)
|
||||
_, err = io.ReadFull(r, buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read string: %w", err)
|
||||
}
|
||||
|
||||
*v = String(string(buf))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *String) writeTo(w io.Writer) error {
|
||||
strBytes := []byte(*v)
|
||||
length := Varint(len(strBytes))
|
||||
|
||||
err := length.writeTo(w)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write string: %w", err)
|
||||
}
|
||||
|
||||
_, err = w.Write(strBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write string: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UnsignedShort uint16
|
||||
|
||||
func (v *UnsignedShort) readFrom(r io.Reader) error {
|
||||
var buf [2]byte
|
||||
_, err := io.ReadFull(r, buf[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("read unsigned short: %w", err)
|
||||
}
|
||||
|
||||
*v = UnsignedShort(buf[0])<<8 | UnsignedShort(buf[1])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *UnsignedShort) writeTo(w io.Writer) error {
|
||||
buf := []byte{byte(*v >> 8), byte(*v & 0xFF)}
|
||||
_, err := w.Write(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write unsigned short: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Long int64
|
||||
|
||||
func (v *Long) readFrom(r io.Reader) error {
|
||||
var buf [8]byte
|
||||
_, err := io.ReadFull(r, buf[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("read long: %w", err)
|
||||
}
|
||||
|
||||
*v = Long(buf[0])<<56 | Long(buf[1])<<48 | Long(buf[2])<<40 | Long(buf[3])<<32 |
|
||||
Long(buf[4])<<24 | Long(buf[5])<<16 | Long(buf[6])<<8 | Long(buf[7])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Long) writeTo(w io.Writer) error {
|
||||
buf := []byte{
|
||||
byte(*v >> 56), byte((*v >> 48) & 0xFF), byte((*v >> 40) & 0xFF), byte((*v >> 32) & 0xFF),
|
||||
byte((*v >> 24) & 0xFF), byte((*v >> 16) & 0xFF), byte((*v >> 8) & 0xFF), byte(*v & 0xFF),
|
||||
}
|
||||
|
||||
_, err := w.Write(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write long: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UUID [16]byte
|
||||
|
||||
func (v *UUID) readFrom(r io.Reader) error {
|
||||
_, err := io.ReadFull(r, v[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("read UUID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Boolean bool
|
||||
|
||||
func (v *Boolean) readFrom(r io.Reader) error {
|
||||
b, err := readByte(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read boolean: %w", err)
|
||||
}
|
||||
if b > 1 {
|
||||
return fmt.Errorf("read boolean: invalid value: %d", b)
|
||||
}
|
||||
*v = b == 1
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Boolean) writeTo(w io.Writer) error {
|
||||
value := byte(0)
|
||||
if *v {
|
||||
value = 1
|
||||
}
|
||||
if _, err := w.Write([]byte{value}); err != nil {
|
||||
return fmt.Errorf("write boolean: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *UUID) writeTo(w io.Writer) error {
|
||||
_, err := w.Write(v[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("write UUID: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Bytes []byte
|
||||
|
||||
func (v *Bytes) readFrom(r io.Reader) error {
|
||||
var length Varint
|
||||
err := length.readFrom(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read bytes: %w", err)
|
||||
}
|
||||
|
||||
if length < 0 || length >= 1024 {
|
||||
return fmt.Errorf("read bytes: invalid size: %d", length)
|
||||
}
|
||||
|
||||
buf := make([]byte, length)
|
||||
|
||||
_, err = io.ReadFull(r, buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read bytes: %w", err)
|
||||
}
|
||||
|
||||
*v = append([]byte(*v), buf...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type RestBytes []byte
|
||||
|
||||
func (v *RestBytes) readFrom(r io.Reader) error {
|
||||
buf, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read remaining bytes: %w", err)
|
||||
}
|
||||
*v = append((*v)[:0], buf...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *RestBytes) writeTo(w io.Writer) error {
|
||||
if _, err := w.Write(*v); err != nil {
|
||||
return fmt.Errorf("write remaining bytes: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Bytes) writeTo(w io.Writer) error {
|
||||
length := Varint(len(*v))
|
||||
err := length.writeTo(w)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write bytes length: %w", err)
|
||||
}
|
||||
|
||||
_, err = w.Write(*v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write bytes: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readByte(r io.Reader) (byte, error) {
|
||||
var buf [1]byte
|
||||
_, err := io.ReadFull(r, buf[:])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read byte: %w", err)
|
||||
}
|
||||
|
||||
return buf[0], nil
|
||||
}
|
||||
|
||||
func writePacket(w io.Writer, packetID int, fields ...field) error {
|
||||
_, err := writePacketWithLength(w, packetID, fields...)
|
||||
return err
|
||||
}
|
||||
|
||||
func writePacketWithLength(w io.Writer, packetID int, fields ...field) (int, error) {
|
||||
frame, err := encodePacket(packetID, fields...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err = writeFull(w, frame); err != nil {
|
||||
return 0, fmt.Errorf("write packet data: %w", err)
|
||||
}
|
||||
return len(frame), nil
|
||||
}
|
||||
|
||||
func encodePacket(packetID int, fields ...field) ([]byte, error) {
|
||||
var dataBuf bytes.Buffer
|
||||
|
||||
for _, field := range fields {
|
||||
err := field.writeTo(&dataBuf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("write packet field: %w", err)
|
||||
}
|
||||
}
|
||||
if dataBuf.Len() > maxPacketDataLength {
|
||||
return nil, fmt.Errorf("write packet: bad length: %d", dataBuf.Len())
|
||||
}
|
||||
|
||||
packetIDVarint := Varint(packetID)
|
||||
bodyLength := varintSize(packetIDVarint) + dataBuf.Len()
|
||||
if bodyLength > maxPacketBodyLength {
|
||||
return nil, fmt.Errorf("write packet: bad length: %d", bodyLength)
|
||||
}
|
||||
|
||||
var frame bytes.Buffer
|
||||
frame.Grow(varintSize(Varint(bodyLength)) + bodyLength)
|
||||
frameLength := Varint(bodyLength)
|
||||
if err := frameLength.writeTo(&frame); err != nil {
|
||||
return nil, fmt.Errorf("write packet length: %w", err)
|
||||
}
|
||||
if err := packetIDVarint.writeTo(&frame); err != nil {
|
||||
return nil, fmt.Errorf("write packet ID: %w", err)
|
||||
}
|
||||
frame.Write(dataBuf.Bytes())
|
||||
return frame.Bytes(), nil
|
||||
}
|
||||
|
||||
func writeFull(w io.Writer, p []byte) error {
|
||||
for len(p) > 0 {
|
||||
n, err := w.Write(p)
|
||||
if n > 0 {
|
||||
p = p[n:]
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeDisconnectPacket(w io.Writer, reason string) error {
|
||||
return writePacket(w, 0x00, new(String(reason)))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadPacketDoesNotConsumeFollowingPacket(t *testing.T) {
|
||||
data := []byte{0x01, 0x80, 0x01, 0x00}
|
||||
r := bytes.NewReader(data)
|
||||
if _, err := readPacket(r); err == nil {
|
||||
t.Fatal("expected truncated packet ID to fail")
|
||||
}
|
||||
pkt, err := readPacket(r)
|
||||
if err != nil {
|
||||
t.Fatalf("read following packet: %v", err)
|
||||
}
|
||||
if pkt.packetID != 0 {
|
||||
t.Fatalf("packet ID = %d", pkt.packetID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketWithLengthReportsWireBytes(t *testing.T) {
|
||||
var wire bytes.Buffer
|
||||
written, err := writePacketWithLength(&wire, 0x03)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if written != 2 || !bytes.Equal(wire.Bytes(), []byte{0x01, 0x03}) {
|
||||
t.Fatalf("wire = %x, length = %d", wire.Bytes(), written)
|
||||
}
|
||||
|
||||
packet, read, err := readPacketWithLength(bytes.NewReader(wire.Bytes()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if packet.packetID != 0x03 || read != written {
|
||||
t.Fatalf("packet ID = %d, read = %d, written = %d", packet.packetID, read, written)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPacketRejectsNonCanonicalLengthVarint(t *testing.T) {
|
||||
_, _, err := readPacketWithLength(bytes.NewReader([]byte{0x81, 0x00, 0x03}))
|
||||
if err == nil || !strings.Contains(err.Error(), "non-canonical") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVarintRejectsOversizedFifthByte(t *testing.T) {
|
||||
var value Varint
|
||||
err := value.readFrom(bytes.NewReader([]byte{0xff, 0xff, 0xff, 0xff, 0x1f}))
|
||||
if err == nil || !strings.Contains(err.Error(), "too large") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/subtle"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Response by vanilla 26.1.2 server.
|
||||
var statusResponse = `{"description":"A Minecraft Server","players":{"max":20,"online":0},"version":{"name":"26.1.2","protocol":775},"enforcesSecureChat":true}`
|
||||
|
||||
type serverState int
|
||||
|
||||
var (
|
||||
serverStateHandshake serverState = 1
|
||||
serverStateProxy serverState = 3
|
||||
)
|
||||
|
||||
type serverConn struct {
|
||||
reader io.Reader
|
||||
writer io.Writer
|
||||
c net.Conn
|
||||
|
||||
state serverState
|
||||
|
||||
handshakeLock sync.Mutex
|
||||
lifecycleMu sync.Mutex
|
||||
closed bool
|
||||
profiles []loginProfile
|
||||
password string
|
||||
rsaPrivateKey *rsa.PrivateKey
|
||||
rsaPublicKey []byte
|
||||
paddingSchedule []paddingTurn
|
||||
packet *packetStream
|
||||
deadlines *connectionDeadlines
|
||||
}
|
||||
|
||||
func (c *serverConn) handshake() error {
|
||||
c.handshakeLock.Lock()
|
||||
defer c.handshakeLock.Unlock()
|
||||
|
||||
if c.state != serverStateHandshake {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.deadlines.beginHandshake(); err != nil {
|
||||
return fmt.Errorf("set deadline: %w", err)
|
||||
}
|
||||
defer func() { _ = c.deadlines.endHandshake() }()
|
||||
|
||||
var (
|
||||
protocolVersion Varint
|
||||
serverAddress String
|
||||
serverPort UnsignedShort
|
||||
nextState Varint
|
||||
)
|
||||
|
||||
// handshake packet
|
||||
|
||||
pkt, err := readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read handshake packet: %w", err)
|
||||
}
|
||||
|
||||
if pkt.packetID != 0 {
|
||||
return fmt.Errorf("bad handshake packet id")
|
||||
}
|
||||
|
||||
err = pkt.readFields(&protocolVersion, &serverAddress, &serverPort, &nextState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read handshake packet: %w", err)
|
||||
}
|
||||
|
||||
switch nextState {
|
||||
case 1:
|
||||
|
||||
// Ping
|
||||
|
||||
for range 2 {
|
||||
|
||||
pkt, err := readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read packet: %w", err)
|
||||
}
|
||||
|
||||
switch pkt.packetID {
|
||||
case 0: // Status Request
|
||||
|
||||
err = writePacket(c.writer, 0, new(String(statusResponse)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("write status response: %w", err)
|
||||
}
|
||||
|
||||
case 1: // Ping
|
||||
|
||||
var payload Long
|
||||
err = pkt.readFields(&payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read ping packet: %w", err)
|
||||
}
|
||||
|
||||
err = writePacket(c.writer, 1, &payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write ping response: %w", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return fmt.Errorf("ping")
|
||||
|
||||
case 2:
|
||||
|
||||
// Login
|
||||
|
||||
// login start
|
||||
|
||||
pkt, err := readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login start packet: %w", err)
|
||||
}
|
||||
|
||||
if pkt.packetID != 0 {
|
||||
return fmt.Errorf("bad login start packet id")
|
||||
}
|
||||
|
||||
var (
|
||||
username String
|
||||
uuid UUID
|
||||
)
|
||||
|
||||
err = pkt.readFields(&username, &uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login start packet: %w", err)
|
||||
}
|
||||
profile, found := findProfile(c.profiles, string(username), uuid)
|
||||
|
||||
// encrypt request
|
||||
|
||||
var (
|
||||
serverId String = String("")
|
||||
publicKey Bytes = Bytes(c.rsaPublicKey)
|
||||
verifyToken Bytes = Bytes(make([]byte, 4))
|
||||
shouldAuthenticate Boolean = true
|
||||
)
|
||||
|
||||
if _, err = rand.Read(verifyToken); err != nil {
|
||||
return fmt.Errorf("generate verify token: %w", err)
|
||||
}
|
||||
|
||||
err = writePacket(c.writer, 0x01, &serverId, &publicKey, &verifyToken, &shouldAuthenticate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write encryption request: %w", err)
|
||||
}
|
||||
|
||||
// encrypt response
|
||||
|
||||
var (
|
||||
encryptedSharedSecret Bytes
|
||||
encryptedVerifyToken Bytes
|
||||
|
||||
sharedSecret []byte
|
||||
decryptedVerifyToken []byte
|
||||
)
|
||||
|
||||
pkt, err = readPacket(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read encrypt response: %w", err)
|
||||
}
|
||||
|
||||
if pkt.packetID != 0x01 {
|
||||
return fmt.Errorf("bad encrypt response packet id")
|
||||
}
|
||||
|
||||
err = pkt.readFields(&encryptedSharedSecret, &encryptedVerifyToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read encrypt response: %w", err)
|
||||
}
|
||||
|
||||
sharedSecret, err = rsa.DecryptPKCS1v15(rand.Reader, c.rsaPrivateKey, encryptedSharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt shared secret: %w", err)
|
||||
}
|
||||
if len(sharedSecret) != 16 {
|
||||
return fmt.Errorf("bad shared secret length: %d", len(sharedSecret))
|
||||
}
|
||||
|
||||
decryptedVerifyToken, err = rsa.DecryptPKCS1v15(rand.Reader, c.rsaPrivateKey, encryptedVerifyToken)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decrypt verify token: %w", err)
|
||||
}
|
||||
|
||||
if len(decryptedVerifyToken) < 4 || !bytes.Equal(verifyToken, decryptedVerifyToken[:4]) {
|
||||
return fmt.Errorf("verify token mismatch")
|
||||
}
|
||||
|
||||
c.reader, err = newCryptoReader(c.reader, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new crypto reader: %w", err)
|
||||
}
|
||||
|
||||
c.writer, err = newCryptoWriter(c.writer, sharedSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("new crypto writer: %w", err)
|
||||
}
|
||||
|
||||
// verify password
|
||||
receivedPassword := decryptedVerifyToken[4:]
|
||||
|
||||
if subtle.ConstantTimeCompare(receivedPassword, []byte(c.password)) != 1 {
|
||||
writeDisconnectPacket(c.writer, `{"type":"translatable","translate":"multiplayer.disconnect.authservers_down"}`)
|
||||
return fmt.Errorf("bad password")
|
||||
}
|
||||
if !found {
|
||||
if err = writeDisconnectPacket(c.writer, `{"text":"You are not white-listed on this server!"}`); err != nil {
|
||||
return fmt.Errorf("write unknown login profile disconnect: %w", err)
|
||||
}
|
||||
return fmt.Errorf("unknown login profile")
|
||||
}
|
||||
|
||||
loginName := String(profile.Username)
|
||||
propertyCount := Varint(1)
|
||||
propertyName := String("textures")
|
||||
texturesValue := String(profile.TexturesValue)
|
||||
signed := Boolean(true)
|
||||
texturesSignature := String(profile.TexturesSignature)
|
||||
if err = writePacket(c.writer, 0x02, &profile.UUID, &loginName, &propertyCount, &propertyName, &texturesValue, &signed, &texturesSignature); err != nil {
|
||||
return fmt.Errorf("write login finished: %w", err)
|
||||
}
|
||||
|
||||
var loginAcknowledgedLength int
|
||||
pkt, loginAcknowledgedLength, err = readPacketWithLength(c.reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read login acknowledged: %w", err)
|
||||
}
|
||||
if err = validateLoginAcknowledgedPacket(pkt); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = runPaddingSchedule(c.reader, c.writer, false, loginAcknowledgedLength, c.paddingSchedule); err != nil {
|
||||
return fmt.Errorf("run startup padding: %w", err)
|
||||
}
|
||||
|
||||
packet := newPacketStream(c.reader, c.writer, false)
|
||||
c.lifecycleMu.Lock()
|
||||
if c.closed {
|
||||
c.lifecycleMu.Unlock()
|
||||
packet.Stop()
|
||||
return net.ErrClosed
|
||||
}
|
||||
c.packet = packet
|
||||
c.reader = packet
|
||||
c.writer = packet
|
||||
c.state = serverStateProxy
|
||||
c.lifecycleMu.Unlock()
|
||||
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("bad handshake packet: bad next state: %d", nextState)
|
||||
}
|
||||
}
|
||||
|
||||
func validateLoginAcknowledgedPacket(pkt *mcPacket) error {
|
||||
if pkt.packetID != 0x03 {
|
||||
return fmt.Errorf("bad login acknowledged packet id: %d", pkt.packetID)
|
||||
}
|
||||
if len(pkt.data) != 0 {
|
||||
return fmt.Errorf("bad login acknowledged packet data length: %d", len(pkt.data))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *serverConn) Read(b []byte) (int, error) {
|
||||
err := c.handshake()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("handshake: %w", err)
|
||||
}
|
||||
|
||||
return c.reader.Read(b)
|
||||
}
|
||||
|
||||
func (c *serverConn) Write(b []byte) (int, error) {
|
||||
err := c.handshake()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("handshake: %w", err)
|
||||
}
|
||||
|
||||
return c.writer.Write(b)
|
||||
}
|
||||
|
||||
func (c *serverConn) Close() error {
|
||||
c.lifecycleMu.Lock()
|
||||
c.closed = true
|
||||
packet := c.packet
|
||||
c.lifecycleMu.Unlock()
|
||||
if packet != nil {
|
||||
packet.Stop()
|
||||
}
|
||||
return c.c.Close()
|
||||
}
|
||||
|
||||
func (c *serverConn) LocalAddr() net.Addr {
|
||||
return c.c.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *serverConn) RemoteAddr() net.Addr {
|
||||
return c.c.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *serverConn) SetDeadline(t time.Time) error {
|
||||
return c.deadlines.setDeadline(t)
|
||||
}
|
||||
|
||||
func (c *serverConn) SetReadDeadline(t time.Time) error {
|
||||
return c.deadlines.setReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *serverConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.deadlines.setWriteDeadline(t)
|
||||
}
|
||||
|
||||
func wrapConnServer(c net.Conn, profiles []loginProfile, password string, rsaPrivateKeyDER []byte, rsaPublicKey []byte) (*serverConn, error) {
|
||||
if len(profiles) == 0 {
|
||||
return nil, fmt.Errorf("empty profiles")
|
||||
}
|
||||
if len(rsaPrivateKeyDER) == 0 {
|
||||
return nil, fmt.Errorf("empty rsa private key")
|
||||
}
|
||||
if len(rsaPublicKey) == 0 {
|
||||
return nil, fmt.Errorf("empty rsa public key")
|
||||
}
|
||||
rsaPrivateKey, err := x509.ParsePKCS1PrivateKey(rsaPrivateKeyDER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse rsa private key: %w", err)
|
||||
}
|
||||
paddingSchedule, err := newServerPaddingSchedule2612()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("select padding profile: %w", err)
|
||||
}
|
||||
|
||||
s := &serverConn{
|
||||
reader: bufio.NewReader(c),
|
||||
writer: c,
|
||||
c: c,
|
||||
state: serverStateHandshake,
|
||||
profiles: profiles,
|
||||
password: password,
|
||||
rsaPrivateKey: rsaPrivateKey,
|
||||
rsaPublicKey: rsaPublicKey,
|
||||
paddingSchedule: paddingSchedule,
|
||||
deadlines: newConnectionDeadlines(c),
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Minecraft stream cipher
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type cryptoStream struct {
|
||||
stream cipher.Stream
|
||||
r io.Reader
|
||||
w io.Writer
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newCryptoReader(r io.Reader, sharedSecret []byte) (*cryptoStream, error) {
|
||||
blockCipher, err := aes.NewCipher(sharedSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new aes cipher: %w", err)
|
||||
}
|
||||
|
||||
stream := newCFB8Decrypt(blockCipher, sharedSecret)
|
||||
|
||||
return &cryptoStream{stream: stream, r: r}, nil
|
||||
}
|
||||
|
||||
func (c *cryptoStream) Read(b []byte) (int, error) {
|
||||
if c.r == nil {
|
||||
panic("read on a write-only crypto stream")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
n, err := c.r.Read(b)
|
||||
if n > 0 {
|
||||
c.stream.XORKeyStream(b[:n], b[:n])
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, fmt.Errorf("crypto reader: read: %w", err)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func newCryptoWriter(w io.Writer, sharedSecret []byte) (*cryptoStream, error) {
|
||||
blockCipher, err := aes.NewCipher(sharedSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new aes cipher: %w", err)
|
||||
}
|
||||
|
||||
stream := newCFB8Encrypt(blockCipher, sharedSecret)
|
||||
|
||||
return &cryptoStream{stream: stream, w: w}, nil
|
||||
}
|
||||
|
||||
func (c *cryptoStream) Write(b []byte) (int, error) {
|
||||
if c.w == nil {
|
||||
panic("write on a read-only crypto stream")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
encrypted := make([]byte, len(b))
|
||||
c.stream.XORKeyStream(encrypted, b)
|
||||
|
||||
if err := writeFull(c.w, encrypted); err != nil {
|
||||
return 0, fmt.Errorf("crypto writer: write: %w", err)
|
||||
}
|
||||
|
||||
return len(b), nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package xmc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type dataAndEOFReader struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (r *dataAndEOFReader) Read(p []byte) (int, error) {
|
||||
if len(r.data) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, r.data)
|
||||
r.data = r.data[n:]
|
||||
return n, io.EOF
|
||||
}
|
||||
|
||||
type shortWriter struct {
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *shortWriter) Write(p []byte) (int, error) {
|
||||
if len(p) > 1 {
|
||||
p = p[:len(p)/2]
|
||||
}
|
||||
return w.Buffer.Write(p)
|
||||
}
|
||||
|
||||
func TestCryptoReaderPreservesDataReturnedWithEOF(t *testing.T) {
|
||||
secret := []byte("0123456789abcdef")
|
||||
plaintext := []byte("payload returned with EOF")
|
||||
var encrypted bytes.Buffer
|
||||
writer, err := newCryptoWriter(&encrypted, secret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = writer.Write(plaintext); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reader, err := newCryptoReader(&dataAndEOFReader{data: encrypted.Bytes()}, secret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := make([]byte, len(plaintext))
|
||||
n, err := reader.Read(got)
|
||||
if err == nil || n != len(plaintext) {
|
||||
t.Fatalf("Read = %d, %v", n, err)
|
||||
}
|
||||
if !bytes.Equal(got[:n], plaintext) {
|
||||
t.Fatalf("plaintext = %q", got[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCryptoWriterHandlesShortWrites(t *testing.T) {
|
||||
secret := []byte("0123456789abcdef")
|
||||
plaintext := bytes.Repeat([]byte("short-write"), 100)
|
||||
var dst shortWriter
|
||||
writer, err := newCryptoWriter(&dst, secret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n, err := writer.Write(plaintext); err != nil || n != len(plaintext) {
|
||||
t.Fatalf("Write = %d, %v", n, err)
|
||||
}
|
||||
reader, err := newCryptoReader(bytes.NewReader(dst.Bytes()), secret)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, plaintext) {
|
||||
t.Fatal("decrypted payload mismatch")
|
||||
}
|
||||
}
|
||||
@@ -136,10 +136,7 @@ func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *in
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
config := tlsConfig.GetTLSConfig()
|
||||
if config.ServerName == "" && address.Family().IsDomain() {
|
||||
config.ServerName = address.Domain()
|
||||
}
|
||||
config := tlsConfig.GetTLSConfig(tls.WithDestination(dest))
|
||||
if fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint); fingerprint != nil {
|
||||
return tls.UClient(c, config, fingerprint), nil
|
||||
} else { // Fallback to normal gRPC TLS
|
||||
|
||||
@@ -38,12 +38,14 @@ func NewHunkReadWriter(hc HunkConn, cancel context.CancelFunc) *HunkReaderWriter
|
||||
|
||||
func NewHunkConn(hc HunkConn, cancel context.CancelFunc, trustedXForwardedFor []string) net.Conn {
|
||||
rAddr := remoteAddrFromContext(hc.Context(), trustedXForwardedFor)
|
||||
lAddr := localAddrFromContext(hc.Context())
|
||||
wrc := NewHunkReadWriter(hc, cancel)
|
||||
return cnc.NewConnection(
|
||||
cnc.ConnectionInput(wrc),
|
||||
cnc.ConnectionOutput(wrc),
|
||||
cnc.ConnectionOnClose(wrc),
|
||||
cnc.ConnectionRemoteAddr(rAddr),
|
||||
cnc.ConnectionLocalAddr(lAddr),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,12 +33,14 @@ func NewMultiHunkReadWriter(hc MultiHunkConn, cancel context.CancelFunc) *MultiH
|
||||
|
||||
func NewMultiHunkConn(hc MultiHunkConn, cancel context.CancelFunc, trustedXForwardedFor []string) net.Conn {
|
||||
rAddr := remoteAddrFromContext(hc.Context(), trustedXForwardedFor)
|
||||
lAddr := localAddrFromContext(hc.Context())
|
||||
wrc := NewMultiHunkReadWriter(hc, cancel)
|
||||
return cnc.NewConnection(
|
||||
cnc.ConnectionInputMulti(wrc),
|
||||
cnc.ConnectionOutputMulti(wrc),
|
||||
cnc.ConnectionOnClose(wrc),
|
||||
cnc.ConnectionRemoteAddr(rAddr),
|
||||
cnc.ConnectionLocalAddr(lAddr),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -56,3 +56,17 @@ func parseTrustedXForwardedFor(md metadata.MD, trusted []string, remoteAddr net.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func localAddrFromContext(ctx context.Context) net.Addr {
|
||||
var localAddr net.Addr
|
||||
if pr, ok := peer.FromContext(ctx); ok {
|
||||
localAddr = pr.LocalAddr
|
||||
}
|
||||
if localAddr == nil {
|
||||
localAddr = &net.TCPAddr{
|
||||
IP: []byte{0, 0, 0, 0},
|
||||
Port: 0,
|
||||
}
|
||||
}
|
||||
return localAddr
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -43,7 +45,11 @@ func (s *server) Handle(conn net.Conn) {
|
||||
|
||||
// upgrade execute a fake websocket upgrade process and return the available connection
|
||||
func (s *server) upgrade(conn net.Conn) (stat.Connection, error) {
|
||||
connReader := bufio.NewReader(conn)
|
||||
// timeout and header limit are the same as websocket
|
||||
conn.SetReadDeadline(time.Now().Add(time.Second * 4))
|
||||
defer conn.SetReadDeadline(time.Time{})
|
||||
connReader := bufio.NewReader(io.LimitReader(conn, 12288))
|
||||
|
||||
req, err := http.ReadRequest(connReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -94,7 +100,15 @@ func (s *server) keepAccepting() {
|
||||
for {
|
||||
conn, err := s.innnerListener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
errStr := err.Error()
|
||||
if strings.Contains(errStr, "closed") {
|
||||
break
|
||||
}
|
||||
errors.LogWarningInner(context.Background(), err, "failed to accept raw connections")
|
||||
if strings.Contains(errStr, "too many") {
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
}
|
||||
continue
|
||||
}
|
||||
go s.Handle(conn)
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
|
||||
c = &client{
|
||||
dest: dest,
|
||||
config: streamSettings.ProtocolSettings.(*Config),
|
||||
tlsConfig: tlsConfig.GetTLSConfig(),
|
||||
tlsConfig: tlsConfig.GetTLSConfig(tls.WithDestination(dest)),
|
||||
socketConfig: streamSettings.SocketSettings,
|
||||
udpmaskManager: streamSettings.UdpmaskManager,
|
||||
quicParams: streamSettings.QuicParams,
|
||||
|
||||
@@ -24,8 +24,11 @@ func (c *Config) GetNormalizedPath() string {
|
||||
path = "/" + path
|
||||
}
|
||||
|
||||
if path[len(path)-1] != '/' {
|
||||
path = path + "/"
|
||||
if c.GetNormalizedSessionPlacement() == PlacementPath ||
|
||||
c.GetNormalizedSeqPlacement() == PlacementPath {
|
||||
if path[len(path)-1] != '/' {
|
||||
path = path + "/"
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
|
||||
@@ -3,16 +3,77 @@ package splithttp_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
. "github.com/xtls/xray-core/transport/internet/splithttp"
|
||||
)
|
||||
|
||||
func Test_GetNormalizedPath(t *testing.T) {
|
||||
c := Config{
|
||||
Path: "/?world",
|
||||
tests := []struct {
|
||||
TestName string
|
||||
Path string
|
||||
SessionIDPlacement string
|
||||
SeqPlacement string
|
||||
Expected string
|
||||
}{
|
||||
{
|
||||
TestName: "default placement keeps trailing slash",
|
||||
Path: "/sh",
|
||||
Expected: "/sh/",
|
||||
},
|
||||
{
|
||||
TestName: "query string is stripped",
|
||||
Path: "/?world",
|
||||
Expected: "/",
|
||||
},
|
||||
{
|
||||
TestName: "both off path drops trailing slash",
|
||||
Path: "/stream",
|
||||
SessionIDPlacement: "query",
|
||||
SeqPlacement: "query",
|
||||
Expected: "/stream",
|
||||
},
|
||||
{
|
||||
TestName: "both off path keeps file-like path",
|
||||
Path: "/stream/filename.extension",
|
||||
SessionIDPlacement: "query",
|
||||
SeqPlacement: "header",
|
||||
Expected: "/stream/filename.extension",
|
||||
},
|
||||
{
|
||||
TestName: "seq in path keeps trailing slash",
|
||||
Path: "/stream",
|
||||
SessionIDPlacement: "query",
|
||||
Expected: "/stream/",
|
||||
},
|
||||
{
|
||||
TestName: "session in path keeps trailing slash",
|
||||
Path: "/stream",
|
||||
SeqPlacement: "cookie",
|
||||
Expected: "/stream/",
|
||||
},
|
||||
{
|
||||
TestName: "existing trailing slash preserved",
|
||||
Path: "/stream/",
|
||||
SessionIDPlacement: "query",
|
||||
SeqPlacement: "query",
|
||||
Expected: "/stream/",
|
||||
},
|
||||
{
|
||||
TestName: "root unchanged",
|
||||
Path: "/",
|
||||
SessionIDPlacement: "query",
|
||||
SeqPlacement: "query",
|
||||
Expected: "/",
|
||||
},
|
||||
}
|
||||
|
||||
path := c.GetNormalizedPath()
|
||||
if path != "/" {
|
||||
t.Error("Unexpected: ", path)
|
||||
for _, test := range tests {
|
||||
t.Run(test.TestName, func(t *testing.T) {
|
||||
c := Config{
|
||||
Path: test.Path,
|
||||
SessionIDPlacement: test.SessionIDPlacement,
|
||||
SeqPlacement: test.SeqPlacement,
|
||||
}
|
||||
assert.Equal(t, test.Expected, c.GetNormalizedPath())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,11 +373,15 @@ func (h *requestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Req
|
||||
Reader: request.Body,
|
||||
ResponseWriter: writer,
|
||||
}
|
||||
localAddr := h.localAddr
|
||||
if la, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && la != nil {
|
||||
localAddr = la
|
||||
}
|
||||
conn := splitConn{
|
||||
writer: httpSC,
|
||||
reader: httpSC,
|
||||
remoteAddr: remoteAddr,
|
||||
localAddr: h.localAddr,
|
||||
localAddr: localAddr,
|
||||
}
|
||||
if sessionId != "" { // if not stream-one
|
||||
conn.reader = currentSession.uploadQueue
|
||||
|
||||
@@ -331,6 +331,9 @@ func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509
|
||||
}
|
||||
|
||||
if verifyResult == foundCA { // if found CA, we need to verify here
|
||||
if len(r.Config.ServerName) == 0 {
|
||||
return errors.New("Pinning CA needs a valid ServerName")
|
||||
}
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: CAs,
|
||||
CurrentTime: time.Now(),
|
||||
@@ -450,9 +453,13 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
|
||||
for _, s := range tls.CipherSuites() {
|
||||
id[s.Name] = s.ID
|
||||
}
|
||||
for _, n := range strings.Split(c.CipherSuites, ":") {
|
||||
if id[n] != 0 {
|
||||
config.CipherSuites = append(config.CipherSuites, id[n])
|
||||
for _, s := range tls.InsecureCipherSuites() {
|
||||
id[s.Name] = s.ID
|
||||
}
|
||||
for n := range strings.SplitSeq(c.CipherSuites, ":") {
|
||||
n = strings.TrimSpace(n)
|
||||
if v, ok := id[n]; ok {
|
||||
config.CipherSuites = append(config.CipherSuites, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user