Compare commits

..
10 Commits
Author SHA1 Message Date
Fangliding 1deae07504 Add grpc local addr 2026-07-21 22:02:44 +08:00
风扇滑翔翼andGitHub 1e9962116f Remove ai comment 2026-07-12 03:49:20 +08:00
echoowall d5dcc4d6fe splithttp: bind sendThrough=origin to the real per-connection local IP
The XHTTP inbound set every accepted connection's LocalAddr to the
listener's address (h.localAddr = l.listener.Addr()). On a wildcard
listener that is the unspecified address ("[::]" / "0.0.0.0").

sendThrough "origin" derives the outbound gateway from inbound.Local,
which comes from conn.LocalAddr(). So with XHTTP every connection's
egress was bound to the wildcard, collapsing all entry IPs onto one
(often IPv6) source address and breaking source-in-source-out on
multi-IP hosts (and failing outright when that address has no route).

Read the concrete per-connection local address from the request context
(http.LocalAddrContextKey), which net/http populates with the address
the client actually connected to. Fall back to the listener address when
the key is absent (e.g. HTTP/3, where net/http does not set it).
2026-07-12 03:39:36 +08:00
RPRXandGitHub 50231eaff9 Xray-core v26.7.11
Sponsor & Donation & NFTs: https://github.com/XTLS/Xray-core/issues/3668
Project X Channel: https://t.me/projectXtls

Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

VLESS Post-Quantum Encryption: https://github.com/XTLS/Xray-core/pull/5067
VLESS NFT: https://opensea.io/collection/vless

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2026-07-11 15:51:51 +00:00
1f74c480d6 Docker: Add pre-release tag (#6470)
https://github.com/XTLS/Xray-core/pull/6470#issuecomment-4947190027

---------

Co-authored-by: KobeArthurScofield <26771058+KobeArthurScofield@users.noreply.github.com>
Co-authored-by: Meow <197331664+Meo597@users.noreply.github.com>
2026-07-11 15:45:59 +00:00
RPRXandGitHub af7eb68028 REALITY server: Set default "minClientVer": "26.3.27" (change it at your own risk)
https://github.com/XTLS/Xray-core/pull/6181#issuecomment-4567373533
https://t.me/projectXtls/3373
https://t.me/projectXtls/3378
2026-07-11 14:30:54 +00:00
Exclude0122andGitHub 35387572e0 Finalmask: Add XMC (TCP, Minecraft) (#6210)
https://github.com/XTLS/Xray-core/pull/6210#issuecomment-4573680059
https://github.com/XTLS/Xray-core/pull/6210#issuecomment-4582554889
https://github.com/XTLS/Xray-core/pull/6210#issuecomment-4937008835
2026-07-11 13:59:24 +00:00
风扇滑翔翼andGitHub 8f15190c23 QUIC sniffer: Fix potential panic on malformed QUIC packets again (#6471)
Fixes https://github.com/XTLS/Xray-core/security/advisories/GHSA-xqmr-94vq-cxvx
Fixes https://github.com/XTLS/Xray-core/security/advisories/GHSA-9h6x-9r9m-5qw2
2026-07-10 21:40:50 +00:00
风扇滑翔翼andGitHub 64fada32b5 TLS client: Pinning CA must have serverName (or higher-priority verifyPeerCertByName or outbound's address) for pinnedPeerCertSha256 (#6472)
Fixes https://github.com/XTLS/Xray-core/security/advisories/GHSA-5wf9-h793-w73c
2026-07-10 21:03:35 +00:00
0bafca9486 Stats: Fix GetOrRegister*() races (#6468)
Fixes https://github.com/XTLS/Xray-core/pull/6467#issue-4856851059

---------

Co-authored-by: Joshua Su <i@joshua.su>
2026-07-10 20:34:27 +00:00
31 changed files with 1764 additions and 54 deletions
+19
View File
@@ -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
+5 -5
View File
@@ -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) })
}
+2 -2
View File
@@ -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
}
+2 -2
View File
@@ -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
View File
@@ -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()
+3 -1
View File
@@ -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)])
+2 -2
View File
@@ -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 = 11
)
var (
+21 -30
View File
@@ -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
+38
View File
@@ -1,9 +1,11 @@
package conf
import (
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/netip"
"net/url"
"regexp"
@@ -22,6 +24,7 @@ import (
"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"
)
@@ -67,6 +70,7 @@ var (
"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{
@@ -715,6 +719,40 @@ func (c *Xdns) Build() (proto.Message, error) {
}, nil
}
type XMC struct {
Hostname string `json:"hostname"`
Usernames []string `json:"usernames"`
Password string `json:"password"`
}
func (c *XMC) Build() (proto.Message, error) {
if len(c.Usernames) == 0 {
c.Usernames = []string{"Dream"}
}
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)
}
return &xmc.Config{
Password: c.Password,
Usernames: c.Usernames,
Hostname: c.Hostname,
RsaPrivateKey: x509.MarshalPKCS1PrivateKey(rsaPrivateKey),
RsaPublicKey: rsaPublicKey,
}, nil
}
type Xicmp struct {
DGRAM bool `json:"dgram"`
IPs []string `json:"ips"`
+2
View File
@@ -113,6 +113,8 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
config.MinClientVer[i] = byte(u)
}
}
} else {
config.MinClientVer = []byte{26, 3, 27} // change it at your own risk: https://github.com/XTLS/Xray-core/pull/6181#issuecomment-4567373533
}
if c.MaxClientVer != "" {
config.MaxClientVer = make([]byte, 3)
+2 -2
View File
@@ -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
}
+2 -2
View File
@@ -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
}
+2 -2
View File
@@ -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
}
+138
View File
@@ -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
}
}
+236
View File
@@ -0,0 +1,236 @@
package xmc
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"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
usernames []string
password string
rsaPublicKey []byte
hostname string
}
type clientState int
var (
clientStateHandshake clientState = 1
clientStateProxy clientState = 2
)
func newClientConn(c net.Conn, usernames []string, password string, rsaPublicKey []byte, hostname string) (*clientConn, error) {
if len(rsaPublicKey) == 0 {
return nil, fmt.Errorf("empty rsa public key")
}
return &clientConn{
reader: bufio.NewReader(c),
writer: c,
c: c,
state: clientStateHandshake,
handshakeLock: sync.Mutex{},
usernames: usernames,
password: password,
rsaPublicKey: rsaPublicKey,
hostname: hostname,
}, nil
}
func (c *clientConn) handshake() error {
c.handshakeLock.Lock()
defer c.handshakeLock.Unlock()
if c.state != clientStateHandshake {
return nil
}
// Handshake timeout
err := c.c.SetDeadline(time.Now().Add(time.Second * 30))
if err != nil {
return fmt.Errorf("set deadline: %w", err)
}
defer c.c.SetDeadline(time.Time{})
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
var (
username string
offlineUUID UUID
)
randomUsername, _ := rand.Int(rand.Reader, big.NewInt(int64(len(c.usernames))))
username = c.usernames[randomUsername.Int64()]
generateOfflineUUID(&offlineUUID, string(username))
err = writePacket(c.writer, 0x00, new(String(username)), &offlineUUID)
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)
rand.Read(sharedSecret)
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)
}
c.state = clientStateProxy
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 {
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.c.SetDeadline(t)
}
func (c *clientConn) SetReadDeadline(t time.Time) error {
return c.c.SetReadDeadline(t)
}
func (c *clientConn) SetWriteDeadline(t time.Time) error {
return c.c.SetWriteDeadline(t)
}
func generateOfflineUUID(uuid *UUID, username string) {
h := sha256.Sum256([]byte("OfflinePlayer:" + username))
copy(uuid[:], h[:16])
uuid[6] = (uuid[6] & 0x0f) | 0x30 // UUID version 3
uuid[8] = (uuid[8] & 0x3f) | 0x80 // UUID variant
}
@@ -0,0 +1,27 @@
package xmc
import (
"fmt"
"net"
)
func (c *Config) TCP() {
}
func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) {
cc, err := newClientConn(conn, c.Usernames, 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) {
cc, err := wrapConnServer(conn, c.Password, c.RsaPrivateKey, c.RsaPublicKey)
if err != nil {
return nil, fmt.Errorf("minecraft finalmask: %w", err)
}
return cc, nil
}
@@ -0,0 +1,160 @@
// 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 Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"`
Usernames []string `protobuf:"bytes,2,rep,name=usernames,proto3" json:"usernames,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"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_transport_internet_finalmask_xmc_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Config) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_transport_internet_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 Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_transport_internet_finalmask_xmc_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetPassword() string {
if x != nil {
return x.Password
}
return ""
}
func (x *Config) GetUsernames() []string {
if x != nil {
return x.Usernames
}
return nil
}
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 ""
}
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\"\xac\x01\n" +
"\x06Config\x12\x1a\n" +
"\bpassword\x18\x01 \x01(\tR\bpassword\x12\x1c\n" +
"\tusernames\x18\x02 \x03(\tR\tusernames\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\bhostnameB\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, 1)
var file_transport_internet_finalmask_xmc_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.transport.internet.finalmask.xmc.Config
}
var file_transport_internet_finalmask_xmc_config_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_transport_internet_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: 1,
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,16 @@
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 Config {
string password = 1;
repeated string usernames = 2;
bytes rsa_private_key = 8;
bytes rsa_public_key = 9;
string hostname = 10;
}
@@ -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,158 @@
package xmc
import (
"bytes"
"crypto/x509"
"net"
"sync"
"testing"
)
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 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"
usernames := []string{"test_user"}
privateKey, publicKey := deriveTestRSAKey(t, password)
go func() {
rawConn, err := ln.Accept()
if err != nil {
return
}
defer rawConn.Close()
server, err := wrapConnServer(rawConn, 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, usernames, 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"
usernames := []string{"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, 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, usernames, clientPassword, serverPublicKey, "localhost")
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
err = client.handshake()
if err != nil {
t.Fatalf("client handshake err: %v", err)
}
_, _ = client.Write([]byte{0x1, 0x2, 0x3, 0x4})
wg.Wait()
// Check if we lost connection or received error
t.Log("Handshake mismatch tested")
}
@@ -0,0 +1,335 @@
// Minecraft protocol
package xmc
import (
"bytes"
"fmt"
"io"
)
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) {
var packetLength Varint
err := packetLength.readFrom(b)
if err != nil {
return nil, fmt.Errorf("read packet length: %w", err)
}
var packetID Varint
err = packetID.readFrom(b)
if err != nil {
return nil, fmt.Errorf("read packet ID: %w", err)
}
dataLength := int(packetLength) - varintSize(packetID)
if dataLength < 0 || dataLength > 1024*32 {
return nil, fmt.Errorf("read packet: bad length: %d", dataLength)
}
data := make([]byte, dataLength)
_, err = io.ReadFull(b, data)
if err != nil {
return nil, fmt.Errorf("read packet data: %w", err)
}
return &mcPacket{
packetID: int(packetID),
data: data,
}, 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
func (v *Varint) readFrom(r io.Reader) error {
SEGMENT_BITS := byte(0x7F)
CONTINUE_BIT := byte(0x80)
var err error
var value int32 = 0
var position int32 = 0
var currentByte byte
for true {
currentByte, err = readByte(r)
if err != nil {
return fmt.Errorf("read varint: %w", err)
}
value |= int32(currentByte&SEGMENT_BITS) << position
if (currentByte & CONTINUE_BIT) == 0 {
break
}
position += 7
if position >= 32 {
return fmt.Errorf("read varint: too large")
}
}
*v = Varint(value)
return nil
}
func (v *Varint) writeTo(w io.Writer) error {
SEGMENT_BITS := byte(0x7F)
CONTINUE_BIT := byte(0x80)
value := int32(*v)
for {
currentByte := byte(value & int32(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 {
size := 0
for {
size++
value >>= 7
if value == 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
}
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", err)
}
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
}
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 {
var dataBuf bytes.Buffer
for _, field := range fields {
err := field.writeTo(&dataBuf)
if err != nil {
return fmt.Errorf("write packet field: %w", err)
}
}
var buf bytes.Buffer
var packetLength Varint = Varint(varintSize(Varint(packetID)) + dataBuf.Len())
err := packetLength.writeTo(&buf)
if err != nil {
return fmt.Errorf("write packet length: %w", err)
}
var packetIDVarint Varint = Varint(packetID)
err = packetIDVarint.writeTo(&buf)
if err != nil {
return fmt.Errorf("write packet ID: %w", err)
}
buf.Write(dataBuf.Bytes())
_, err = w.Write(buf.Bytes())
if err != nil {
return fmt.Errorf("write packet data: %w", err)
}
return nil
}
func writeDisconnectPacket(w io.Writer, reason string) error {
return writePacket(w, 0x00, new(String(reason)))
}
+289
View File
@@ -0,0 +1,289 @@
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
password string
rsaPrivateKey *rsa.PrivateKey
rsaPublicKey []byte
}
func (c *serverConn) handshake() error {
c.handshakeLock.Lock()
defer c.handshakeLock.Unlock()
if c.state != serverStateHandshake {
return nil
}
// handshake timeout
err := c.c.SetDeadline(time.Now().Add(time.Second * 30))
if err != nil {
return fmt.Errorf("set deadline: %w", err)
}
defer c.c.SetDeadline(time.Time{})
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)
}
// encrypt request
var (
serverId String = String("")
publicKey Bytes = Bytes(c.rsaPublicKey)
verifyToken Bytes = Bytes(make([]byte, 4))
shouldAuthenticate Varint = Varint(1)
)
rand.Read(verifyToken)
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)
}
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")
}
c.state = serverStateProxy
return nil
default:
return fmt.Errorf("bad handshake packet: bad next state: %d", nextState)
}
}
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 {
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.c.SetDeadline(t)
}
func (c *serverConn) SetReadDeadline(t time.Time) error {
return c.c.SetReadDeadline(t)
}
func (c *serverConn) SetWriteDeadline(t time.Time) error {
return c.c.SetWriteDeadline(t)
}
func wrapConnServer(c net.Conn, password string, rsaPrivateKeyDER []byte, rsaPublicKey []byte) (*serverConn, error) {
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)
}
s := &serverConn{
reader: bufio.NewReader(c),
writer: c,
c: c,
state: serverStateHandshake,
password: password,
rsaPrivateKey: rsaPrivateKey,
rsaPublicKey: rsaPublicKey,
}
return s, nil
}
@@ -0,0 +1,68 @@
// Minecraft stream cipher
package xmc
import (
"crypto/aes"
"crypto/cipher"
"fmt"
"io"
)
type cryptoStream struct {
stream cipher.Stream
r io.Reader
w io.Writer
}
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")
}
n, err := c.r.Read(b)
if err != nil {
return 0, fmt.Errorf("crypto reader: read: %w", err)
}
c.stream.XORKeyStream(b[:n], b[:n])
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")
}
encrypted := make([]byte, len(b))
c.stream.XORKeyStream(encrypted, b)
n, err := c.w.Write(encrypted)
if err != nil {
return 0, fmt.Errorf("crypto writer: write: %w", err)
}
return n, nil
}
+1 -4
View File
@@ -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
}
+1 -1
View File
@@ -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,
+5 -1
View File
@@ -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
+3
View File
@@ -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(),