Files
Singbox-For-SBPanel/protocol/sbproxy/outbound.go
2026-04-22 02:18:51 +08:00

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)
}