mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-21 08:44:26 +00:00
Packets of one client session can be processed by two inbound workers at the same time (e.g. the client's old and new source address after a NAT rebinding). The sliding window was checked and updated in two separate critical sections, and clientBodyCipher and the session's User were read without the lock, so the same packet ID could be accepted more than once and go test -race reported data races. Re-check the window and publish the cached body cipher in one critical section after the packet is fully validated, and always initialize the session's User under the lock.
270 lines
7.2 KiB
Go
270 lines
7.2 KiB
Go
package shadowsocks_2022
|
|
|
|
import (
|
|
"context"
|
|
"crypto/cipher"
|
|
"encoding/binary"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/xtls/xray-core/common"
|
|
"github.com/xtls/xray-core/common/buf"
|
|
"github.com/xtls/xray-core/common/errors"
|
|
"github.com/xtls/xray-core/common/log"
|
|
"github.com/xtls/xray-core/common/net"
|
|
"github.com/xtls/xray-core/common/protocol"
|
|
"github.com/xtls/xray-core/common/session"
|
|
"github.com/xtls/xray-core/common/uuid"
|
|
"github.com/xtls/xray-core/core"
|
|
"github.com/xtls/xray-core/features/policy"
|
|
"github.com/xtls/xray-core/features/routing"
|
|
"github.com/xtls/xray-core/transport/internet/stat"
|
|
)
|
|
|
|
func init() {
|
|
common.Must(common.RegisterConfig((*RelayServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
|
return NewRelayServer(ctx, config.(*RelayServerConfig))
|
|
}))
|
|
}
|
|
|
|
type relayDest struct {
|
|
destination net.Destination
|
|
email string
|
|
level uint32
|
|
blockCipher cipher.Block
|
|
}
|
|
|
|
type RelayInbound struct {
|
|
networks []net.Network
|
|
method *CipherMethod
|
|
relayPSK []byte
|
|
relayBlock cipher.Block
|
|
destinations map[[AESBlockSize]byte]*relayDest
|
|
udpSessions *UDPSessionManager
|
|
policyManager policy.Manager
|
|
}
|
|
|
|
func NewRelayServer(ctx context.Context, config *RelayServerConfig) (*RelayInbound, error) {
|
|
networks := config.Network
|
|
if len(networks) == 0 {
|
|
networks = []net.Network{
|
|
net.Network_TCP,
|
|
net.Network_UDP,
|
|
}
|
|
}
|
|
|
|
method, err := GetCipherMethod(config.Method)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if method.IsChaCha {
|
|
return nil, errors.New("shadowsocks 2022 relay: only aes methods are supported")
|
|
}
|
|
|
|
relayPSK, err := ParseKey(config.Key, method.KeySaltLength)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
relayBlock, err := method.NewBlock(relayPSK)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
v := core.MustFromContext(ctx)
|
|
i := &RelayInbound{
|
|
networks: networks,
|
|
method: method,
|
|
relayPSK: relayPSK,
|
|
relayBlock: relayBlock,
|
|
destinations: make(map[[AESBlockSize]byte]*relayDest),
|
|
udpSessions: NewUDPSessionManager(500 * time.Second),
|
|
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
|
|
}
|
|
|
|
for idx, d := range config.Destinations {
|
|
if d.Email == "" {
|
|
u := uuid.New()
|
|
d.Email = "unnamed-destination-" + strconv.Itoa(idx) + "-" + u.String()
|
|
}
|
|
destKey, err := ParseKey(d.Key, method.KeySaltLength)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
destBlock, err := method.NewBlock(destKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hash := DeriveUserPSKHash(destKey)
|
|
|
|
i.destinations[hash] = &relayDest{
|
|
destination: net.TCPDestination(d.Address.AsAddress(), net.Port(d.Port)),
|
|
email: d.Email,
|
|
level: uint32(d.Level),
|
|
blockCipher: destBlock,
|
|
}
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
func (i *RelayInbound) Network() []net.Network {
|
|
return i.networks
|
|
}
|
|
|
|
func (i *RelayInbound) Process(ctx context.Context, network net.Network, connection stat.Connection, dispatcher routing.Dispatcher) error {
|
|
inbound := session.InboundFromContext(ctx)
|
|
inbound.Name = "shadowsocks-2022-relay"
|
|
inbound.CanSpliceCopy = 3
|
|
|
|
if network == net.Network_TCP {
|
|
return i.processTCP(ctx, connection, dispatcher)
|
|
}
|
|
return i.processUDP(ctx, connection, dispatcher)
|
|
}
|
|
|
|
func (i *RelayInbound) processTCP(ctx context.Context, conn net.Conn, dispatcher routing.Dispatcher) error {
|
|
defer conn.Close()
|
|
|
|
sessionPolicy := i.policyManager.ForLevel(0)
|
|
if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
|
|
return errors.New("unable to set read deadline").Base(err)
|
|
}
|
|
|
|
// Read initial handshake in a single read call per SIP022 §3.1.3 & §3.1.4
|
|
needed := i.method.KeySaltLength + AESBlockSize
|
|
requestHeader := buf.New()
|
|
n, err := requestHeader.ReadFrom(conn)
|
|
if err != nil {
|
|
requestHeader.Release()
|
|
ResetTCPConn(conn)
|
|
return err
|
|
}
|
|
if int(n) < needed {
|
|
requestHeader.Release()
|
|
ResetTCPConn(conn)
|
|
return ErrInvalidRequest
|
|
}
|
|
|
|
headerSlice := requestHeader.Bytes()
|
|
salt := headerSlice[:i.method.KeySaltLength]
|
|
eih := headerSlice[i.method.KeySaltLength:needed]
|
|
|
|
decryptedHash, err := DecryptEIH(i.method, i.relayPSK, salt, eih)
|
|
if err != nil {
|
|
requestHeader.Release()
|
|
ResetTCPConn(conn)
|
|
return err
|
|
}
|
|
|
|
targetDest, ok := i.destinations[decryptedHash]
|
|
if !ok {
|
|
requestHeader.Release()
|
|
ResetTCPConn(conn)
|
|
return ErrInvalidRequest
|
|
}
|
|
conn.SetReadDeadline(time.Time{})
|
|
|
|
inbound := session.InboundFromContext(ctx)
|
|
inbound.User = &protocol.MemoryUser{
|
|
Email: targetDest.email,
|
|
Level: targetDest.level,
|
|
}
|
|
|
|
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
|
From: conn.RemoteAddr(),
|
|
To: targetDest.destination,
|
|
Status: log.AccessAccepted,
|
|
Email: targetDest.email,
|
|
})
|
|
|
|
errors.LogInfo(ctx, "relaying connection to ", targetDest.destination)
|
|
|
|
link, err := dispatcher.Dispatch(ctx, targetDest.destination)
|
|
if err != nil {
|
|
requestHeader.Release()
|
|
return err
|
|
}
|
|
|
|
// Unwrap outer EIH: send client salt and remaining handshake bytes to next hop
|
|
// in a single write call, satisfying downstream server's single-read handshake expectation (SIP022 §3.1.3).
|
|
var saltCopy [32]byte
|
|
copy(saltCopy[:i.method.KeySaltLength], salt)
|
|
copy(requestHeader.Bytes()[AESBlockSize:AESBlockSize+i.method.KeySaltLength], saltCopy[:i.method.KeySaltLength])
|
|
requestHeader.Advance(AESBlockSize)
|
|
|
|
if err := link.Writer.WriteMultiBuffer(buf.MultiBuffer{requestHeader}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return TransportTCP(ctx, i.policyManager.ForLevel(targetDest.level), buf.NewReader(conn), buf.NewWriter(conn), link)
|
|
}
|
|
|
|
func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error {
|
|
reader := buf.NewPacketReader(conn)
|
|
for {
|
|
mb, err := reader.ReadMultiBuffer()
|
|
if err != nil {
|
|
buf.ReleaseMulti(mb)
|
|
return err
|
|
}
|
|
|
|
for _, b := range mb {
|
|
data := b.Bytes()
|
|
if len(data) < 2*AESBlockSize {
|
|
b.Release()
|
|
continue
|
|
}
|
|
|
|
var packetHeader [AESBlockSize]byte
|
|
i.relayBlock.Decrypt(packetHeader[:], data[:AESBlockSize])
|
|
|
|
eiHeader := DecryptUDPEIH(i.relayBlock, packetHeader[:], data[AESBlockSize:2*AESBlockSize])
|
|
|
|
targetDest, ok := i.destinations[eiHeader]
|
|
if !ok {
|
|
b.Release()
|
|
continue
|
|
}
|
|
|
|
// Extract sessionID from raw packetHeader for session-level link caching before re-encrypting
|
|
sessionID := binary.BigEndian.Uint64(packetHeader[:8])
|
|
|
|
// Re-encrypt packetHeader with next hop block cipher
|
|
targetDest.blockCipher.Encrypt(packetHeader[:], packetHeader[:])
|
|
|
|
// Strip outer EIH: replace second block with re-encrypted packetHeader and advance
|
|
copy(data[AESBlockSize:2*AESBlockSize], packetHeader[:])
|
|
b.Advance(int32(AESBlockSize))
|
|
|
|
dest := targetDest.destination
|
|
dest.Network = net.Network_UDP
|
|
|
|
sessionItem := i.udpSessions.GetOrCreate(sessionID)
|
|
sessionItem.Lock()
|
|
if sessionItem.User == nil {
|
|
sessionItem.User = &protocol.MemoryUser{
|
|
Email: targetDest.email,
|
|
Level: targetDest.level,
|
|
}
|
|
}
|
|
sessionItem.Unlock()
|
|
link := sessionItem.link.Load()
|
|
if link != nil {
|
|
sessionItem.UpdateConn(conn)
|
|
} else {
|
|
var err error
|
|
link, err = sessionItem.EnsureLink(ctx, conn, dest, dispatcher, i.policyManager, nil)
|
|
if err != nil {
|
|
b.Release()
|
|
continue
|
|
}
|
|
}
|
|
|
|
_ = link.Writer.WriteMultiBuffer(buf.MultiBuffer{b})
|
|
}
|
|
}
|
|
}
|