363 lines
9.5 KiB
Go
363 lines
9.5 KiB
Go
package sbproxy
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/adapter"
|
|
"github.com/sagernet/sing-box/adapter/outbound"
|
|
"github.com/sagernet/sing-box/common/dialer"
|
|
C "github.com/sagernet/sing-box/constant"
|
|
"github.com/sagernet/sing-box/log"
|
|
"github.com/sagernet/sing-box/option"
|
|
"github.com/sandertv/go-raknet"
|
|
M "github.com/sagernet/sing/common/metadata"
|
|
N "github.com/sagernet/sing/common/network"
|
|
)
|
|
|
|
func RegisterOutbound(registry *outbound.Registry) {
|
|
outbound.Register[option.SBProxyOutboundOptions](registry, C.TypeSBProxy, NewOutbound)
|
|
}
|
|
|
|
type Outbound struct {
|
|
outbound.Adapter
|
|
ctx context.Context
|
|
logger log.ContextLogger
|
|
options option.SBProxyOutboundOptions
|
|
dialer N.Dialer
|
|
serverAddr M.Socksaddr
|
|
}
|
|
|
|
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SBProxyOutboundOptions) (adapter.Outbound, error) {
|
|
if strings.TrimSpace(options.Password) == "" {
|
|
return nil, errors.New("sbproxy outbound password is required")
|
|
}
|
|
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Outbound{
|
|
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSBProxy, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
|
ctx: ctx,
|
|
logger: logger,
|
|
options: options,
|
|
dialer: outboundDialer,
|
|
serverAddr: options.ServerOptions.Build(),
|
|
}, nil
|
|
}
|
|
|
|
func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
conn, err := h.dialer.DialContext(ctx, N.NetworkTCP, h.serverAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 1. Java MC Handshake (State 2: Login)
|
|
var handshake bytes.Buffer
|
|
WriteVarInt(&handshake, 763) // Version
|
|
WriteString(&handshake, h.serverAddr.AddrString())
|
|
binary.Write(&handshake, binary.BigEndian, uint16(h.serverAddr.Port))
|
|
WriteVarInt(&handshake, 2)
|
|
|
|
h.sendPacket(conn, 0x00, handshake.Bytes())
|
|
|
|
// 2. Login Start
|
|
var loginStart bytes.Buffer
|
|
username := h.options.Username
|
|
if username == "" { username = "user" }
|
|
WriteString(&loginStart, username)
|
|
h.sendPacket(conn, 0x00, loginStart.Bytes())
|
|
|
|
// 3. Consume LoginSuccess packet.
|
|
// We must consume the full packet body; reading only packet length and ID
|
|
// leaves unread bytes on the stream and breaks subsequent challenge parsing.
|
|
loginPacketLen, _, err := ReadVarInt(conn)
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
if loginPacketLen <= 0 {
|
|
_ = conn.Close()
|
|
return nil, io.ErrUnexpectedEOF
|
|
}
|
|
loginPacket := make([]byte, int(loginPacketLen))
|
|
if _, err = io.ReadFull(conn, loginPacket); err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
loginReader := bytes.NewReader(loginPacket)
|
|
loginPacketID, _, err := ReadVarInt(loginReader)
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
if loginPacketID != 0x02 { // Login Success
|
|
_ = conn.Close()
|
|
return nil, io.ErrUnexpectedEOF
|
|
}
|
|
|
|
// 4. Connect with SBProxy logic
|
|
playConn, err := h.handleClientPlay(ctx, conn, false)
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
if cpc, ok := playConn.(*clientPlayConn); ok {
|
|
if err := cpc.sendDestination(destination); err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
}
|
|
return playConn, nil
|
|
}
|
|
|
|
func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
|
conn, err := raknet.Dial(h.serverAddr.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 1. Bedrock Login (Manual 0x01)
|
|
if _, err = conn.Write([]byte{0x01}); err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
// 2. Wrap as play
|
|
return h.handleClientPlayPacket(ctx, conn)
|
|
}
|
|
|
|
func (h *Outbound) handleClientPlay(ctx context.Context, conn net.Conn, isUDP bool) (net.Conn, error) {
|
|
// 1. Receive Encryption Challenge from Server
|
|
payload, err := h.readClientPacket(conn, isUDP, JavaChannelEncryption)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(payload) < 24 {
|
|
return nil, io.ErrUnexpectedEOF
|
|
}
|
|
|
|
_ = int64(binary.BigEndian.Uint64(payload[:8])) // serverTime
|
|
serverNonce := payload[8:24]
|
|
|
|
// 2. Respond with Client Nonce and Time
|
|
clientNonce := make([]byte, 16)
|
|
rand.Read(clientNonce)
|
|
|
|
now := time.Now().Unix()
|
|
var response bytes.Buffer
|
|
binary.Write(&response, binary.BigEndian, now)
|
|
response.Write(clientNonce)
|
|
|
|
err = h.sendClientPacket(conn, isUDP, JavaChannelEncryption, response.Bytes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 3. Setup Encryption
|
|
sessionSalt := make([]byte, 16)
|
|
for i := 0; i < 16; i++ {
|
|
sessionSalt[i] = serverNonce[i] ^ clientNonce[i]
|
|
}
|
|
encrypter, err := NewCipherStream(h.options.Password, sessionSalt, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
decrypter, err := NewCipherStream(h.options.Password, sessionSalt, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &clientPlayConn{
|
|
Conn: conn,
|
|
encrypter: encrypter,
|
|
decrypter: decrypter,
|
|
isUDP: isUDP,
|
|
outbound: h,
|
|
}, nil
|
|
}
|
|
|
|
func (h *Outbound) sendPacket(w io.Writer, id int32, data []byte) error {
|
|
var body bytes.Buffer
|
|
WriteVarInt(&body, id)
|
|
body.Write(data)
|
|
WriteVarInt(w, int32(body.Len()))
|
|
_, err := w.Write(body.Bytes())
|
|
return err
|
|
}
|
|
|
|
func (h *Outbound) handleClientPlayPacket(ctx context.Context, conn net.Conn) (net.PacketConn, error) {
|
|
c, err := h.handleClientPlay(ctx, conn, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &clientPlayPacketConn{
|
|
clientPlayConn: c.(*clientPlayConn),
|
|
}, nil
|
|
}
|
|
|
|
func (h *Outbound) sendClientPacket(w io.Writer, isUDP bool, channel string, data []byte) error {
|
|
if isUDP {
|
|
_, err := w.Write(append([]byte{BedrockPacketTunnel}, data...))
|
|
return err
|
|
}
|
|
var body bytes.Buffer
|
|
WriteString(&body, channel)
|
|
body.Write(data)
|
|
var packet bytes.Buffer
|
|
WriteVarInt(&packet, 0x0D) // Java Plugin Message (Play state usually 0x0D or 0x0C depending on ver, here matching server 0x0D)
|
|
packet.Write(body.Bytes())
|
|
WriteVarInt(w, int32(packet.Len()))
|
|
_, err := w.Write(packet.Bytes())
|
|
return err
|
|
}
|
|
|
|
func (h *Outbound) readClientPacket(r io.Reader, isUDP bool, channel string) ([]byte, error) {
|
|
if isUDP {
|
|
buf := make([]byte, 2048)
|
|
n, err := r.Read(buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if n == 0 || buf[0] != BedrockPacketTunnel {
|
|
return nil, io.ErrUnexpectedEOF
|
|
}
|
|
return buf[1:n], nil
|
|
}
|
|
pLen, _, err := ReadVarInt(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if pLen <= 0 {
|
|
return nil, io.ErrUnexpectedEOF
|
|
}
|
|
packet := make([]byte, int(pLen))
|
|
if _, err = io.ReadFull(r, packet); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reader := bytes.NewReader(packet)
|
|
pID, _, err := ReadVarInt(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if pID != 0x18 { // Java Plugin Message (Server -> Client in Play is 0x18)
|
|
return nil, fmt.Errorf("sbproxy: unexpected packet id before challenge, got=0x%x expected=0x18", pID)
|
|
}
|
|
ch, err := ReadString(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ch != channel {
|
|
return nil, fmt.Errorf("sbproxy: unexpected channel before challenge, got=%q expected=%q", ch, channel)
|
|
}
|
|
remaining := reader.Len()
|
|
if remaining < 0 {
|
|
return nil, errors.New("invalid client packet payload length")
|
|
}
|
|
payload := make([]byte, remaining)
|
|
if _, err = io.ReadFull(reader, payload); err != nil {
|
|
return nil, err
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
type clientPlayConn struct {
|
|
net.Conn
|
|
encrypter cipher.Stream
|
|
decrypter cipher.Stream
|
|
isUDP bool
|
|
outbound *Outbound
|
|
readRemain []byte
|
|
}
|
|
|
|
func destinationString(destination M.Socksaddr) (string, error) {
|
|
host := strings.TrimSpace(destination.AddrString())
|
|
if host == "" {
|
|
return "", errors.New("sbproxy: destination host is empty")
|
|
}
|
|
if destination.Port == 0 {
|
|
return "", errors.New("sbproxy: destination port is empty")
|
|
}
|
|
return net.JoinHostPort(host, fmt.Sprintf("%d", destination.Port)), nil
|
|
}
|
|
|
|
func (c *clientPlayConn) sendDestination(destination M.Socksaddr) error {
|
|
destinationText, err := destinationString(destination)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
payload := []byte(destinationText)
|
|
encrypted := make([]byte, len(payload))
|
|
c.encrypter.XORKeyStream(encrypted, payload)
|
|
return c.outbound.sendClientPacket(c.Conn, c.isUDP, JavaChannelData, encrypted)
|
|
}
|
|
|
|
func (c *clientPlayConn) Read(b []byte) (int, error) {
|
|
// Return buffered data from previous oversized read first
|
|
if len(c.readRemain) > 0 {
|
|
n := copy(b, c.readRemain)
|
|
c.readRemain = c.readRemain[n:]
|
|
if len(c.readRemain) == 0 {
|
|
c.readRemain = nil
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
for {
|
|
var payload []byte
|
|
var err error
|
|
if c.isUDP {
|
|
payload, err = c.outbound.readClientPacket(c.Conn, true, "")
|
|
} else {
|
|
payload, err = c.outbound.readClientPacket(c.Conn, false, JavaChannelData)
|
|
}
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(payload) == 0 {
|
|
continue // Retry instead of returning (0, nil) which violates io.Reader
|
|
}
|
|
c.decrypter.XORKeyStream(payload, payload)
|
|
n := copy(b, payload)
|
|
if n < len(payload) {
|
|
// Buffer the remaining data for the next Read call
|
|
c.readRemain = make([]byte, len(payload)-n)
|
|
copy(c.readRemain, payload[n:])
|
|
}
|
|
return n, nil
|
|
}
|
|
}
|
|
|
|
func (c *clientPlayConn) Write(b []byte) (int, error) {
|
|
encrypted := make([]byte, len(b))
|
|
c.encrypter.XORKeyStream(encrypted, b)
|
|
err := c.outbound.sendClientPacket(c.Conn, c.isUDP, JavaChannelData, encrypted)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return len(b), nil
|
|
}
|
|
|
|
type clientPlayPacketConn struct {
|
|
*clientPlayConn
|
|
}
|
|
|
|
func (c *clientPlayPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
|
n, err := c.clientPlayConn.Read(b)
|
|
return n, c.clientPlayConn.RemoteAddr(), err
|
|
}
|
|
|
|
func (c *clientPlayPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
|
return c.clientPlayConn.Write(b)
|
|
}
|