add minecraft protocol
This commit is contained in:
@@ -0,0 +1,462 @@
|
||||
--- /dev/null
|
||||
+++ b/protocol/minecraft/inbound.go
|
||||
@@ -0,0 +1,459 @@
|
||||
+// OMV
|
||||
+package minecraft
|
||||
+
|
||||
+import (
|
||||
+ "bufio"
|
||||
+ "bytes"
|
||||
+ "context"
|
||||
+ "crypto/rsa"
|
||||
+ "io"
|
||||
+ "net"
|
||||
+ "os"
|
||||
+
|
||||
+ "github.com/sagernet/sing-box/adapter"
|
||||
+ "github.com/sagernet/sing-box/adapter/inbound"
|
||||
+ "github.com/sagernet/sing-box/common/listener"
|
||||
+ "github.com/sagernet/sing-box/common/uot"
|
||||
+ C "github.com/sagernet/sing-box/constant"
|
||||
+ "github.com/sagernet/sing-box/log"
|
||||
+ "github.com/sagernet/sing-box/option"
|
||||
+ "github.com/sagernet/sing/common"
|
||||
+ E "github.com/sagernet/sing/common/exceptions"
|
||||
+ "github.com/sagernet/sing/common/logger"
|
||||
+ M "github.com/sagernet/sing/common/metadata"
|
||||
+ N "github.com/sagernet/sing/common/network"
|
||||
+ "github.com/sagernet/sing/common/task"
|
||||
+ "github.com/sagernet/smux"
|
||||
+)
|
||||
+
|
||||
+func RegisterInbound(registry *inbound.Registry) {
|
||||
+ inbound.Register[option.MinecraftInboundOptions](registry, C.TypeMinecraft, NewInbound)
|
||||
+}
|
||||
+
|
||||
+var _ adapter.TCPInjectableInbound = (*Inbound)(nil)
|
||||
+
|
||||
+type Inbound struct {
|
||||
+ inbound.Adapter
|
||||
+ router adapter.ConnectionRouterEx
|
||||
+ logger logger.ContextLogger
|
||||
+ listener *listener.Listener
|
||||
+ privateKey *rsa.PrivateKey
|
||||
+ publicKey []byte
|
||||
+ users map[string]string // username -> password
|
||||
+ status option.MinecraftStatus
|
||||
+ fallbackAddr M.Socksaddr
|
||||
+ fallbackTag string
|
||||
+}
|
||||
+
|
||||
+func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MinecraftInboundOptions) (adapter.Inbound, error) {
|
||||
+ privateKey, err := generateRSAKeyPair()
|
||||
+ if err != nil {
|
||||
+ return nil, E.Cause(err, "generate RSA key pair")
|
||||
+ }
|
||||
+
|
||||
+ publicKeyBytes, err := marshalPublicKey(&privateKey.PublicKey)
|
||||
+ if err != nil {
|
||||
+ return nil, E.Cause(err, "marshal public key")
|
||||
+ }
|
||||
+
|
||||
+ users := make(map[string]string)
|
||||
+ for _, user := range options.Users {
|
||||
+ users[user.Username] = user.Password
|
||||
+ }
|
||||
+
|
||||
+ var status option.MinecraftStatus
|
||||
+ if options.Status != nil {
|
||||
+ status = *options.Status
|
||||
+ }
|
||||
+
|
||||
+ h := &Inbound{
|
||||
+ Adapter: inbound.NewAdapter(C.TypeMinecraft, tag),
|
||||
+ router: uot.NewRouter(router, logger),
|
||||
+ logger: logger,
|
||||
+ privateKey: privateKey,
|
||||
+ publicKey: publicKeyBytes,
|
||||
+ users: users,
|
||||
+ status: status,
|
||||
+ }
|
||||
+
|
||||
+ if options.Fallback != nil {
|
||||
+ h.fallbackAddr = options.Fallback.ServerOptions.Build()
|
||||
+ if h.fallbackAddr.Port == 0 {
|
||||
+ h.fallbackAddr.Port = 25565
|
||||
+ }
|
||||
+ h.fallbackTag = options.Fallback.Tag
|
||||
+ }
|
||||
+
|
||||
+ h.listener = listener.New(listener.Options{
|
||||
+ Context: ctx,
|
||||
+ Logger: logger,
|
||||
+ Network: []string{N.NetworkTCP},
|
||||
+ Listen: options.ListenOptions,
|
||||
+ ConnectionHandler: h,
|
||||
+ })
|
||||
+ return h, nil
|
||||
+}
|
||||
+
|
||||
+func (h *Inbound) Start(stage adapter.StartStage) error {
|
||||
+ if stage != adapter.StartStateStart {
|
||||
+ return nil
|
||||
+ }
|
||||
+ return h.listener.Start()
|
||||
+}
|
||||
+
|
||||
+func (h *Inbound) Close() error {
|
||||
+ return common.Close(h.listener)
|
||||
+}
|
||||
+
|
||||
+func (h *Inbound) hasFallback() bool {
|
||||
+ return h.fallbackAddr.IsValid()
|
||||
+}
|
||||
+
|
||||
+func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
+ err := h.handleConnection(ctx, conn, metadata, onClose)
|
||||
+ if err != nil && !E.IsClosed(err) {
|
||||
+ h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func (h *Inbound) handleConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
||||
+ br := bufio.NewReader(conn)
|
||||
+
|
||||
+ firstByte, err := br.Peek(1)
|
||||
+ if err != nil {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "peek first byte")
|
||||
+ }
|
||||
+
|
||||
+ // Legacy ping (0xFE): forward or respond locally
|
||||
+ if firstByte[0] == legacyPingByte {
|
||||
+ if h.status.Forward && h.hasFallback() {
|
||||
+ return h.doFallback(ctx, conn, br, nil, metadata, onClose)
|
||||
+ }
|
||||
+ return h.handleLegacyPing(conn, onClose)
|
||||
+ }
|
||||
+
|
||||
+ // Read handshake
|
||||
+ hsID, hsData, err := readPacket(br)
|
||||
+ if err != nil {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "read handshake")
|
||||
+ }
|
||||
+ if hsID != packetHandshake {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, nil)
|
||||
+ return E.New("expected handshake packet, got ", hsID)
|
||||
+ }
|
||||
+
|
||||
+ handshake, err := readHandshake(hsData)
|
||||
+ if err != nil {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "parse handshake")
|
||||
+ }
|
||||
+
|
||||
+ switch handshake.NextState {
|
||||
+ case stateStatus:
|
||||
+ if h.status.Forward && h.hasFallback() {
|
||||
+ return h.doFallback(ctx, conn, br, []rawPacket{{hsID, hsData}}, metadata, onClose)
|
||||
+ }
|
||||
+ return h.handleStatus(br, conn, onClose)
|
||||
+ case stateLogin:
|
||||
+ return h.handleLogin(ctx, br, conn, metadata, onClose, hsID, hsData)
|
||||
+ default:
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, nil)
|
||||
+ return E.New("unknown next state: ", handshake.NextState)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// rawPacket holds a consumed MC packet (id + payload) for replay
|
||||
+type rawPacket struct {
|
||||
+ id int32
|
||||
+ data []byte
|
||||
+}
|
||||
+
|
||||
+// doFallback replays consumed packets and relays the connection to the fallback MC server
|
||||
+// via the sing-box router (so routing rules / outbounds apply).
|
||||
+func (h *Inbound) doFallback(ctx context.Context, conn net.Conn, br *bufio.Reader, consumed []rawPacket, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
||||
+ var prefix bytes.Buffer
|
||||
+ for _, pkt := range consumed {
|
||||
+ writePacket(&prefix, pkt.id, pkt.data)
|
||||
+ }
|
||||
+
|
||||
+ if h.fallbackTag != "" {
|
||||
+ metadata.Inbound = h.fallbackTag
|
||||
+ } else {
|
||||
+ metadata.Inbound = h.Tag()
|
||||
+ }
|
||||
+ metadata.InboundType = h.Type()
|
||||
+ metadata.Destination = h.fallbackAddr
|
||||
+
|
||||
+ h.logger.InfoContext(ctx, "fallback connection to ", h.fallbackAddr)
|
||||
+ h.router.RouteConnectionEx(ctx, newPrefixConn(conn, prefix.Bytes(), br), metadata, onClose)
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
+func (h *Inbound) handleLegacyPing(conn net.Conn, onClose N.CloseHandlerFunc) error {
|
||||
+ defer func() {
|
||||
+ conn.Close()
|
||||
+ if onClose != nil {
|
||||
+ onClose(nil)
|
||||
+ }
|
||||
+ }()
|
||||
+ resp := encodeLegacyPingResponse(&h.status)
|
||||
+ _, err := conn.Write(resp)
|
||||
+ return err
|
||||
+}
|
||||
+
|
||||
+func (h *Inbound) handleStatus(br *bufio.Reader, conn net.Conn, onClose N.CloseHandlerFunc) error {
|
||||
+ defer func() {
|
||||
+ conn.Close()
|
||||
+ if onClose != nil {
|
||||
+ onClose(nil)
|
||||
+ }
|
||||
+ }()
|
||||
+
|
||||
+ packetID, _, err := readPacket(br)
|
||||
+ if err != nil {
|
||||
+ return E.Cause(err, "read status request")
|
||||
+ }
|
||||
+ if packetID != packetStatusRequest {
|
||||
+ return E.New("expected status request, got ", packetID)
|
||||
+ }
|
||||
+
|
||||
+ responseData := encodeStatusResponse(&h.status)
|
||||
+ if err := writePacket(conn, packetStatusResponse, responseData); err != nil {
|
||||
+ return E.Cause(err, "write status response")
|
||||
+ }
|
||||
+
|
||||
+ packetID, data, err := readPacket(br)
|
||||
+ if err != nil {
|
||||
+ return nil
|
||||
+ }
|
||||
+ if packetID != packetPingRequest {
|
||||
+ return nil
|
||||
+ }
|
||||
+ return writePacket(conn, packetPingResponse, data)
|
||||
+}
|
||||
+
|
||||
+func (h *Inbound) handleLogin(ctx context.Context, br *bufio.Reader, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc, hsID int32, hsData []byte) error {
|
||||
+ // Read Login Start
|
||||
+ lsID, lsData, err := readPacket(br)
|
||||
+ if err != nil {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "read login start")
|
||||
+ }
|
||||
+ if lsID != packetLoginStart {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, nil)
|
||||
+ return E.New("expected login start, got ", lsID)
|
||||
+ }
|
||||
+
|
||||
+ loginStart, err := readLoginStart(lsData)
|
||||
+ if err != nil {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "parse login start")
|
||||
+ }
|
||||
+
|
||||
+ username := loginStart.Name
|
||||
+
|
||||
+ password, ok := h.users[username]
|
||||
+ if !ok {
|
||||
+ if h.hasFallback() {
|
||||
+ return h.doFallback(ctx, conn, br, []rawPacket{
|
||||
+ {hsID, hsData},
|
||||
+ {lsID, lsData},
|
||||
+ }, metadata, onClose)
|
||||
+ }
|
||||
+ h.sendDisconnect(conn, "Failed to verify username!")
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, nil)
|
||||
+ return E.New("unknown user: ", username)
|
||||
+ }
|
||||
+
|
||||
+ // Generate verify token
|
||||
+ verifyToken, err := generateVerifyToken()
|
||||
+ if err != nil {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "generate verify token")
|
||||
+ }
|
||||
+
|
||||
+ // Send Encryption Request
|
||||
+ encReqData := encodeEncryptionRequest("", h.publicKey, verifyToken)
|
||||
+ if err := writePacket(conn, packetEncryptionRequest, encReqData); err != nil {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "write encryption request")
|
||||
+ }
|
||||
+
|
||||
+ // Read Encryption Response (via br to drain any buffered data)
|
||||
+ packetID, data, err := readPacket(br)
|
||||
+ if err != nil {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "read encryption response")
|
||||
+ }
|
||||
+ if packetID != packetEncryptionResponse {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, nil)
|
||||
+ return E.New("expected encryption response, got ", packetID)
|
||||
+ }
|
||||
+
|
||||
+ encResp, err := readEncryptionResponse(data)
|
||||
+ if err != nil {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "parse encryption response")
|
||||
+ }
|
||||
+
|
||||
+ // Decrypt shared secret and verify token
|
||||
+ sharedSecret, err := rsa.DecryptPKCS1v15(nil, h.privateKey, encResp.SharedSecret)
|
||||
+ if err != nil {
|
||||
+ h.sendDisconnect(conn, "Failed to verify username!")
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "decrypt shared secret")
|
||||
+ }
|
||||
+
|
||||
+ decryptedToken, err := rsa.DecryptPKCS1v15(nil, h.privateKey, encResp.VerifyToken)
|
||||
+ if err != nil {
|
||||
+ h.sendDisconnect(conn, "Failed to verify username!")
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "decrypt verify token")
|
||||
+ }
|
||||
+
|
||||
+ if !bytes.Equal(decryptedToken, verifyToken) {
|
||||
+ h.sendDisconnect(conn, "Failed to verify username!")
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, nil)
|
||||
+ return E.New("verify token mismatch")
|
||||
+ }
|
||||
+
|
||||
+ expectedSecret := deriveSharedSecret(password, verifyToken)
|
||||
+ if !bytes.Equal(sharedSecret, expectedSecret) {
|
||||
+ h.sendDisconnect(conn, "Failed to verify username!")
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, nil)
|
||||
+ return E.New("authentication failed for user: ", username)
|
||||
+ }
|
||||
+
|
||||
+ // Enable encryption — wrap with readerConn so br's buffer is drained properly
|
||||
+ encConn, err := newEncryptedConn(&readerConn{Conn: conn, reader: br}, sharedSecret)
|
||||
+ if err != nil {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "enable encryption")
|
||||
+ }
|
||||
+
|
||||
+ // Send Login Success (encrypted)
|
||||
+ loginSuccessData := encodeLoginSuccess(username)
|
||||
+ if err := writePacket(encConn, packetLoginSuccess, loginSuccessData); err != nil {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "write login success")
|
||||
+ }
|
||||
+
|
||||
+ // Read Login Acknowledged (1.20.2+)
|
||||
+ _, _, err = readPacket(encConn)
|
||||
+ if err != nil {
|
||||
+ N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
+ return E.Cause(err, "read login acknowledged")
|
||||
+ }
|
||||
+
|
||||
+ h.logger.InfoContext(ctx, "Minecraft login completed for user ", username, " from ", metadata.Source)
|
||||
+
|
||||
+ return h.handleMuxSession(ctx, encConn, metadata.Source, onClose, username)
|
||||
+}
|
||||
+
|
||||
+func (h *Inbound) sendDisconnect(conn net.Conn, reason string) {
|
||||
+ _ = writePacket(conn, packetLoginDisconnect, encodeLoginDisconnect(reason))
|
||||
+}
|
||||
+
|
||||
+func (h *Inbound) handleMuxSession(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc, user string) error {
|
||||
+ session, err := smux.Server(conn, smuxConfig())
|
||||
+ if err != nil {
|
||||
+ if onClose != nil {
|
||||
+ onClose(err)
|
||||
+ }
|
||||
+ return err
|
||||
+ }
|
||||
+ var group task.Group
|
||||
+ group.Append0(func(_ context.Context) error {
|
||||
+ for {
|
||||
+ stream, sErr := session.AcceptStream()
|
||||
+ if sErr != nil {
|
||||
+ return sErr
|
||||
+ }
|
||||
+ go h.handleMuxStream(ctx, stream, source, user)
|
||||
+ }
|
||||
+ })
|
||||
+ group.Cleanup(func() {
|
||||
+ session.Close()
|
||||
+ if onClose != nil {
|
||||
+ onClose(os.ErrClosed)
|
||||
+ }
|
||||
+ })
|
||||
+ return group.Run(ctx)
|
||||
+}
|
||||
+
|
||||
+func (h *Inbound) handleMuxStream(ctx context.Context, conn net.Conn, source M.Socksaddr, user string) {
|
||||
+ err := h.handleMuxStream0(ctx, conn, source, user)
|
||||
+ if err != nil {
|
||||
+ h.logger.ErrorContext(ctx, E.Cause(err, "process mux stream"))
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func (h *Inbound) handleMuxStream0(ctx context.Context, conn net.Conn, source M.Socksaddr, user string) error {
|
||||
+ var cmdBuf [1]byte
|
||||
+ _, err := conn.Read(cmdBuf[:])
|
||||
+ if err != nil {
|
||||
+ return E.Cause(err, "read command")
|
||||
+ }
|
||||
+ command := cmdBuf[0]
|
||||
+
|
||||
+ destination, err := M.SocksaddrSerializer.ReadAddrPort(conn)
|
||||
+ if err != nil {
|
||||
+ return E.Cause(err, "read destination")
|
||||
+ }
|
||||
+
|
||||
+ var metadata adapter.InboundContext
|
||||
+ metadata.Inbound = h.Tag()
|
||||
+ metadata.InboundType = h.Type()
|
||||
+ metadata.Source = source
|
||||
+ metadata.User = user
|
||||
+
|
||||
+ switch command {
|
||||
+ case commandTCP:
|
||||
+ metadata.Destination = destination
|
||||
+ h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
|
||||
+ h.router.RouteConnectionEx(ctx, conn, metadata, nil)
|
||||
+ case commandUDP:
|
||||
+ metadata.Destination = destination
|
||||
+ h.logger.InfoContext(ctx, "inbound UoT packet connection to ", metadata.Destination)
|
||||
+ h.router.RouteConnectionEx(ctx, conn, metadata, nil)
|
||||
+ default:
|
||||
+ return E.New("unknown command ", command)
|
||||
+ }
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
+// prefixConn replays prefix bytes, then drains remaining buffered data, then reads from conn.
|
||||
+// Writes go directly to the underlying conn.
|
||||
+type prefixConn struct {
|
||||
+ net.Conn
|
||||
+ reader io.Reader
|
||||
+}
|
||||
+
|
||||
+func newPrefixConn(conn net.Conn, prefix []byte, remaining io.Reader) *prefixConn {
|
||||
+ var readers []io.Reader
|
||||
+ if len(prefix) > 0 {
|
||||
+ readers = append(readers, bytes.NewReader(prefix))
|
||||
+ }
|
||||
+ readers = append(readers, remaining)
|
||||
+ return &prefixConn{
|
||||
+ Conn: conn,
|
||||
+ reader: io.MultiReader(readers...),
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func (c *prefixConn) Read(b []byte) (int, error) {
|
||||
+ return c.reader.Read(b)
|
||||
+}
|
||||
+
|
||||
+// readerConn overrides Read to use a different io.Reader (e.g. a bufio.Reader)
|
||||
+// while keeping all other net.Conn methods on the underlying conn.
|
||||
+type readerConn struct {
|
||||
+ net.Conn
|
||||
+ reader io.Reader
|
||||
+}
|
||||
+
|
||||
+func (c *readerConn) Read(b []byte) (int, error) {
|
||||
+ return c.reader.Read(b)
|
||||
+}
|
||||
@@ -0,0 +1,319 @@
|
||||
--- /dev/null
|
||||
+++ b/protocol/minecraft/outbound.go
|
||||
@@ -0,0 +1,316 @@
|
||||
+// OMV
|
||||
+package minecraft
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "crypto/rand"
|
||||
+ "crypto/rsa"
|
||||
+ "net"
|
||||
+ "sync"
|
||||
+
|
||||
+ "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/sagernet/sing/common"
|
||||
+ E "github.com/sagernet/sing/common/exceptions"
|
||||
+ "github.com/sagernet/sing/common/logger"
|
||||
+ M "github.com/sagernet/sing/common/metadata"
|
||||
+ N "github.com/sagernet/sing/common/network"
|
||||
+ "github.com/sagernet/sing/common/uot"
|
||||
+ "github.com/sagernet/smux"
|
||||
+)
|
||||
+
|
||||
+func RegisterOutbound(registry *outbound.Registry) {
|
||||
+ outbound.Register[option.MinecraftOutboundOptions](registry, C.TypeMinecraft, NewOutbound)
|
||||
+}
|
||||
+
|
||||
+var _ adapter.InterfaceUpdateListener = (*Outbound)(nil)
|
||||
+
|
||||
+type Outbound struct {
|
||||
+ outbound.Adapter
|
||||
+ ctx context.Context
|
||||
+ logger logger.ContextLogger
|
||||
+ dialer N.Dialer
|
||||
+ serverAddr M.Socksaddr
|
||||
+ username string
|
||||
+ password string
|
||||
+
|
||||
+ sessionAccess sync.Mutex
|
||||
+ session *muxSession
|
||||
+ nextSession uint32
|
||||
+}
|
||||
+
|
||||
+type muxSession struct {
|
||||
+ session *smux.Session
|
||||
+ conn net.Conn
|
||||
+}
|
||||
+
|
||||
+func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MinecraftOutboundOptions) (adapter.Outbound, error) {
|
||||
+ outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+
|
||||
+ ob := &Outbound{
|
||||
+ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeMinecraft, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
||||
+ ctx: ctx,
|
||||
+ logger: logger,
|
||||
+ dialer: outboundDialer,
|
||||
+ serverAddr: options.ServerOptions.Build(),
|
||||
+ username: options.Username,
|
||||
+ password: options.Password,
|
||||
+ }
|
||||
+
|
||||
+ if ob.serverAddr.Port == 0 {
|
||||
+ ob.serverAddr.Port = 25565
|
||||
+ }
|
||||
+
|
||||
+ if ob.username == "" {
|
||||
+ ob.username = "Steve"
|
||||
+ }
|
||||
+
|
||||
+ return ob, nil
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) createSession() (*muxSession, error) {
|
||||
+ h.logger.InfoContext(h.ctx, "creating Minecraft session to ", h.serverAddr)
|
||||
+
|
||||
+ conn, err := h.dialer.DialContext(h.ctx, N.NetworkTCP, h.serverAddr)
|
||||
+ if err != nil {
|
||||
+ return nil, E.Cause(err, "dial server")
|
||||
+ }
|
||||
+
|
||||
+ // Send Handshake
|
||||
+ handshakeData := encodeHandshake(&handshakePacket{
|
||||
+ ProtocolVersion: protocolVersion,
|
||||
+ ServerAddress: h.serverAddr.AddrString(),
|
||||
+ ServerPort: h.serverAddr.Port,
|
||||
+ NextState: stateLogin,
|
||||
+ })
|
||||
+ if err := writePacket(conn, packetHandshake, handshakeData); err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, E.Cause(err, "write handshake")
|
||||
+ }
|
||||
+
|
||||
+ // Send Login Start
|
||||
+ loginStartData := encodeLoginStart(h.username)
|
||||
+ if err := writePacket(conn, packetLoginStart, loginStartData); err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, E.Cause(err, "write login start")
|
||||
+ }
|
||||
+
|
||||
+ // Read Encryption Request
|
||||
+ packetID, data, err := readPacket(conn)
|
||||
+ if err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, E.Cause(err, "read encryption request")
|
||||
+ }
|
||||
+
|
||||
+ // Check for disconnect
|
||||
+ if packetID == packetLoginDisconnect {
|
||||
+ conn.Close()
|
||||
+ return nil, E.New("server disconnected during login")
|
||||
+ }
|
||||
+
|
||||
+ if packetID != packetEncryptionRequest {
|
||||
+ conn.Close()
|
||||
+ return nil, E.New("expected encryption request, got ", packetID)
|
||||
+ }
|
||||
+
|
||||
+ encReq, err := readEncryptionRequest(data)
|
||||
+ if err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, E.Cause(err, "parse encryption request")
|
||||
+ }
|
||||
+
|
||||
+ // Parse server's public key
|
||||
+ serverPubKey, err := parsePublicKey(encReq.PublicKey)
|
||||
+ if err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, E.Cause(err, "parse server public key")
|
||||
+ }
|
||||
+
|
||||
+ // Derive shared secret from password + verify token
|
||||
+ sharedSecret := deriveSharedSecret(h.password, encReq.VerifyToken)
|
||||
+
|
||||
+ // Encrypt shared secret with server's public key
|
||||
+ encryptedSecret, err := rsa.EncryptPKCS1v15(rand.Reader, serverPubKey, sharedSecret)
|
||||
+ if err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, E.Cause(err, "encrypt shared secret")
|
||||
+ }
|
||||
+
|
||||
+ // Encrypt verify token with server's public key
|
||||
+ encryptedToken, err := rsa.EncryptPKCS1v15(rand.Reader, serverPubKey, encReq.VerifyToken)
|
||||
+ if err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, E.Cause(err, "encrypt verify token")
|
||||
+ }
|
||||
+
|
||||
+ // Send Encryption Response
|
||||
+ encRespData := encodeEncryptionResponse(encryptedSecret, encryptedToken)
|
||||
+ if err := writePacket(conn, packetEncryptionResponse, encRespData); err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, E.Cause(err, "write encryption response")
|
||||
+ }
|
||||
+
|
||||
+ // Enable encryption
|
||||
+ encConn, err := newEncryptedConn(conn, sharedSecret)
|
||||
+ if err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, E.Cause(err, "enable encryption")
|
||||
+ }
|
||||
+
|
||||
+ // Read Login Success
|
||||
+ packetID, _, err = readPacket(encConn)
|
||||
+ if err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, E.Cause(err, "read login success")
|
||||
+ }
|
||||
+ if packetID != packetLoginSuccess {
|
||||
+ conn.Close()
|
||||
+ return nil, E.New("expected login success, got ", packetID)
|
||||
+ }
|
||||
+
|
||||
+ // Send Login Acknowledged (packet ID 0x03, empty data)
|
||||
+ if err := writePacket(encConn, 0x03, nil); err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, E.Cause(err, "write login acknowledged")
|
||||
+ }
|
||||
+
|
||||
+ // Create smux session over encrypted connection
|
||||
+ session, err := smux.Client(encConn, smuxConfig())
|
||||
+ if err != nil {
|
||||
+ conn.Close()
|
||||
+ return nil, E.Cause(err, "create mux session")
|
||||
+ }
|
||||
+
|
||||
+ return &muxSession{session: session, conn: conn}, nil
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) getSession() (*smux.Session, error) {
|
||||
+ h.sessionAccess.Lock()
|
||||
+ defer h.sessionAccess.Unlock()
|
||||
+
|
||||
+ if h.session != nil && !h.session.session.IsClosed() {
|
||||
+ return h.session.session, nil
|
||||
+ }
|
||||
+ if h.session != nil {
|
||||
+ _ = common.Close(h.session.session, h.session.conn)
|
||||
+ h.session = nil
|
||||
+ }
|
||||
+
|
||||
+ entry, err := h.createSession()
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ h.session = entry
|
||||
+
|
||||
+ go func(session *smux.Session, conn net.Conn) {
|
||||
+ <-session.CloseChan()
|
||||
+ h.sessionAccess.Lock()
|
||||
+ if h.session != nil && h.session.session == session {
|
||||
+ h.session = nil
|
||||
+ }
|
||||
+ h.sessionAccess.Unlock()
|
||||
+ _ = common.Close(session, conn)
|
||||
+ }(entry.session, entry.conn)
|
||||
+
|
||||
+ return entry.session, nil
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) invalidateSession(session *smux.Session) {
|
||||
+ h.sessionAccess.Lock()
|
||||
+ defer h.sessionAccess.Unlock()
|
||||
+
|
||||
+ if h.session != nil && h.session.session == session {
|
||||
+ _ = common.Close(h.session.session, h.session.conn)
|
||||
+ h.session = nil
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) openStream(ctx context.Context, command byte, destination M.Socksaddr) (net.Conn, error) {
|
||||
+ _ = ctx
|
||||
+ // Only 1 session since server limits connections to 2 (we use 1)
|
||||
+ for i := 0; i < 2; i++ {
|
||||
+ session, err := h.getSession()
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+
|
||||
+ stream, err := session.OpenStream()
|
||||
+ if err != nil {
|
||||
+ h.invalidateSession(session)
|
||||
+ continue
|
||||
+ }
|
||||
+
|
||||
+ _, err = stream.Write([]byte{command})
|
||||
+ if err != nil {
|
||||
+ stream.Close()
|
||||
+ continue
|
||||
+ }
|
||||
+ err = M.SocksaddrSerializer.WriteAddrPort(stream, destination)
|
||||
+ if err != nil {
|
||||
+ stream.Close()
|
||||
+ continue
|
||||
+ }
|
||||
+
|
||||
+ return stream, nil
|
||||
+ }
|
||||
+ return nil, E.New("failed to open mux stream")
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
+ switch N.NetworkName(network) {
|
||||
+ case N.NetworkTCP:
|
||||
+ h.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||
+ return h.openStream(ctx, commandTCP, destination)
|
||||
+ case N.NetworkUDP:
|
||||
+ h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
|
||||
+ conn, err := h.openStream(ctx, commandUDP, uot.RequestDestination(uot.Version))
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ return uot.NewLazyConn(conn, uot.Request{
|
||||
+ IsConnect: true,
|
||||
+ Destination: destination,
|
||||
+ }), nil
|
||||
+ default:
|
||||
+ return nil, E.New("unsupported network: ", network)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
+ h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
|
||||
+ conn, err := h.openStream(ctx, commandUDP, uot.RequestDestination(uot.Version))
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ return uot.NewLazyConn(conn, uot.Request{
|
||||
+ IsConnect: false,
|
||||
+ Destination: destination,
|
||||
+ }), nil
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) InterfaceUpdated() {
|
||||
+ h.sessionAccess.Lock()
|
||||
+ defer h.sessionAccess.Unlock()
|
||||
+ if h.session != nil {
|
||||
+ _ = common.Close(h.session.session, h.session.conn)
|
||||
+ h.session = nil
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func (h *Outbound) Close() error {
|
||||
+ h.sessionAccess.Lock()
|
||||
+ defer h.sessionAccess.Unlock()
|
||||
+ if h.session != nil {
|
||||
+ err := common.Close(h.session.session, h.session.conn)
|
||||
+ h.session = nil
|
||||
+ return err
|
||||
+ }
|
||||
+ return nil
|
||||
+}
|
||||
@@ -0,0 +1,659 @@
|
||||
--- /dev/null
|
||||
+++ b/protocol/minecraft/protocol.go
|
||||
@@ -0,0 +1,656 @@
|
||||
+// OMV
|
||||
+package minecraft
|
||||
+
|
||||
+import (
|
||||
+ "bytes"
|
||||
+ "crypto/aes"
|
||||
+ "crypto/cipher"
|
||||
+ "crypto/rand"
|
||||
+ "crypto/rsa"
|
||||
+ "crypto/sha1"
|
||||
+ "crypto/sha256"
|
||||
+ "crypto/x509"
|
||||
+ "encoding/binary"
|
||||
+ "encoding/json"
|
||||
+ "fmt"
|
||||
+ "io"
|
||||
+ "net"
|
||||
+
|
||||
+ "github.com/sagernet/sing-box/option"
|
||||
+ "github.com/sagernet/smux"
|
||||
+)
|
||||
+
|
||||
+// Minecraft protocol constants
|
||||
+const (
|
||||
+ protocolVersion = 765 // 1.20.4
|
||||
+ versionName = "1.20.4"
|
||||
+ maxPacketSize = 2097151 // 2^21 - 1
|
||||
+
|
||||
+ stateHandshake = 0
|
||||
+ stateStatus = 1
|
||||
+ stateLogin = 2
|
||||
+
|
||||
+ // Handshake state packets
|
||||
+ packetHandshake = 0x00
|
||||
+
|
||||
+ // Status state packets
|
||||
+ packetStatusRequest = 0x00
|
||||
+ packetStatusResponse = 0x00
|
||||
+ packetPingRequest = 0x01
|
||||
+ packetPingResponse = 0x01
|
||||
+
|
||||
+ // Login state packets
|
||||
+ packetLoginStart = 0x00
|
||||
+ packetEncryptionRequest = 0x01
|
||||
+ packetEncryptionResponse = 0x01
|
||||
+ packetLoginSuccess = 0x02
|
||||
+ packetLoginDisconnect = 0x00
|
||||
+
|
||||
+ rsaKeyBits = 1024
|
||||
+ verifyTokenLen = 4
|
||||
+ sharedSecretLen = 16
|
||||
+)
|
||||
+
|
||||
+// VarInt encoding/decoding
|
||||
+
|
||||
+func readVarInt(r io.Reader) (int32, error) {
|
||||
+ var result int32
|
||||
+ var shift uint
|
||||
+ buf := make([]byte, 1)
|
||||
+ for {
|
||||
+ _, err := io.ReadFull(r, buf)
|
||||
+ if err != nil {
|
||||
+ return 0, err
|
||||
+ }
|
||||
+ b := buf[0]
|
||||
+ result |= int32(b&0x7F) << shift
|
||||
+ if b&0x80 == 0 {
|
||||
+ break
|
||||
+ }
|
||||
+ shift += 7
|
||||
+ if shift >= 35 {
|
||||
+ return 0, fmt.Errorf("VarInt too big")
|
||||
+ }
|
||||
+ }
|
||||
+ return result, nil
|
||||
+}
|
||||
+
|
||||
+func writeVarInt(w io.Writer, value int32) error {
|
||||
+ buf := encodeVarInt(value)
|
||||
+ _, err := w.Write(buf)
|
||||
+ return err
|
||||
+}
|
||||
+
|
||||
+func encodeVarInt(value int32) []byte {
|
||||
+ var buf [5]byte
|
||||
+ n := 0
|
||||
+ uv := uint32(value)
|
||||
+ for {
|
||||
+ b := byte(uv & 0x7F)
|
||||
+ uv >>= 7
|
||||
+ if uv != 0 {
|
||||
+ b |= 0x80
|
||||
+ }
|
||||
+ buf[n] = b
|
||||
+ n++
|
||||
+ if uv == 0 {
|
||||
+ break
|
||||
+ }
|
||||
+ }
|
||||
+ return buf[:n]
|
||||
+}
|
||||
+
|
||||
+func varIntLen(value int32) int {
|
||||
+ return len(encodeVarInt(value))
|
||||
+}
|
||||
+
|
||||
+// Packet reading/writing
|
||||
+
|
||||
+func readPacket(r io.Reader) (packetID int32, data []byte, err error) {
|
||||
+ length, err := readVarInt(r)
|
||||
+ if err != nil {
|
||||
+ return 0, nil, err
|
||||
+ }
|
||||
+ if length < 0 || length > maxPacketSize {
|
||||
+ return 0, nil, fmt.Errorf("invalid packet length: %d", length)
|
||||
+ }
|
||||
+ payload := make([]byte, length)
|
||||
+ _, err = io.ReadFull(r, payload)
|
||||
+ if err != nil {
|
||||
+ return 0, nil, err
|
||||
+ }
|
||||
+ pr := bytes.NewReader(payload)
|
||||
+ packetID, err = readVarInt(pr)
|
||||
+ if err != nil {
|
||||
+ return 0, nil, err
|
||||
+ }
|
||||
+ data = payload[varIntLen(packetID):]
|
||||
+ return packetID, data, nil
|
||||
+}
|
||||
+
|
||||
+func writePacket(w io.Writer, packetID int32, data []byte) error {
|
||||
+ idBytes := encodeVarInt(packetID)
|
||||
+ totalLen := int32(len(idBytes) + len(data))
|
||||
+ if err := writeVarInt(w, totalLen); err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ if _, err := w.Write(idBytes); err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ if _, err := w.Write(data); err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
+// String encoding (VarInt length + UTF-8 bytes)
|
||||
+
|
||||
+func readString(r io.Reader) (string, error) {
|
||||
+ length, err := readVarInt(r)
|
||||
+ if err != nil {
|
||||
+ return "", err
|
||||
+ }
|
||||
+ if length < 0 || length > 32767 {
|
||||
+ return "", fmt.Errorf("string too long: %d", length)
|
||||
+ }
|
||||
+ buf := make([]byte, length)
|
||||
+ _, err = io.ReadFull(r, buf)
|
||||
+ if err != nil {
|
||||
+ return "", err
|
||||
+ }
|
||||
+ return string(buf), nil
|
||||
+}
|
||||
+
|
||||
+func encodeString(s string) []byte {
|
||||
+ var buf bytes.Buffer
|
||||
+ buf.Write(encodeVarInt(int32(len(s))))
|
||||
+ buf.WriteString(s)
|
||||
+ return buf.Bytes()
|
||||
+}
|
||||
+
|
||||
+// Byte array encoding (VarInt length + bytes)
|
||||
+
|
||||
+func readByteArray(r io.Reader) ([]byte, error) {
|
||||
+ length, err := readVarInt(r)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ if length < 0 || length > 1048576 {
|
||||
+ return nil, fmt.Errorf("byte array too long: %d", length)
|
||||
+ }
|
||||
+ buf := make([]byte, length)
|
||||
+ _, err = io.ReadFull(r, buf)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ return buf, nil
|
||||
+}
|
||||
+
|
||||
+func encodeByteArray(data []byte) []byte {
|
||||
+ var buf bytes.Buffer
|
||||
+ buf.Write(encodeVarInt(int32(len(data))))
|
||||
+ buf.Write(data)
|
||||
+ return buf.Bytes()
|
||||
+}
|
||||
+
|
||||
+// Handshake packet
|
||||
+
|
||||
+type handshakePacket struct {
|
||||
+ ProtocolVersion int32
|
||||
+ ServerAddress string
|
||||
+ ServerPort uint16
|
||||
+ NextState int32
|
||||
+}
|
||||
+
|
||||
+func readHandshake(data []byte) (*handshakePacket, error) {
|
||||
+ r := bytes.NewReader(data)
|
||||
+ pv, err := readVarInt(r)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ addr, err := readString(r)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ var port uint16
|
||||
+ if err := binary.Read(r, binary.BigEndian, &port); err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ ns, err := readVarInt(r)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ return &handshakePacket{
|
||||
+ ProtocolVersion: pv,
|
||||
+ ServerAddress: addr,
|
||||
+ ServerPort: port,
|
||||
+ NextState: ns,
|
||||
+ }, nil
|
||||
+}
|
||||
+
|
||||
+func encodeHandshake(h *handshakePacket) []byte {
|
||||
+ var buf bytes.Buffer
|
||||
+ buf.Write(encodeVarInt(h.ProtocolVersion))
|
||||
+ buf.Write(encodeString(h.ServerAddress))
|
||||
+ binary.Write(&buf, binary.BigEndian, h.ServerPort)
|
||||
+ buf.Write(encodeVarInt(h.NextState))
|
||||
+ return buf.Bytes()
|
||||
+}
|
||||
+
|
||||
+// Login Start packet
|
||||
+
|
||||
+type loginStartPacket struct {
|
||||
+ Name string
|
||||
+ UUID [16]byte
|
||||
+}
|
||||
+
|
||||
+func readLoginStart(data []byte) (*loginStartPacket, error) {
|
||||
+ r := bytes.NewReader(data)
|
||||
+ name, err := readString(r)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ pkt := &loginStartPacket{Name: name}
|
||||
+ // Read UUID (16 bytes) if available
|
||||
+ if r.Len() >= 16 {
|
||||
+ io.ReadFull(r, pkt.UUID[:])
|
||||
+ }
|
||||
+ return pkt, nil
|
||||
+}
|
||||
+
|
||||
+func encodeLoginStart(name string) []byte {
|
||||
+ var buf bytes.Buffer
|
||||
+ buf.Write(encodeString(name))
|
||||
+ // Write zero UUID
|
||||
+ buf.Write(make([]byte, 16))
|
||||
+ return buf.Bytes()
|
||||
+}
|
||||
+
|
||||
+// Encryption Request packet
|
||||
+
|
||||
+type encryptionRequestPacket struct {
|
||||
+ ServerID string
|
||||
+ PublicKey []byte
|
||||
+ VerifyToken []byte
|
||||
+}
|
||||
+
|
||||
+func encodeEncryptionRequest(serverID string, pubKey []byte, verifyToken []byte) []byte {
|
||||
+ var buf bytes.Buffer
|
||||
+ buf.Write(encodeString(serverID))
|
||||
+ buf.Write(encodeByteArray(pubKey))
|
||||
+ buf.Write(encodeByteArray(verifyToken))
|
||||
+ // Note: ShouldAuthenticate field was added in 1.20.5 (protocol 766).
|
||||
+ // We target 1.20.4 (protocol 765), so this field is absent.
|
||||
+ return buf.Bytes()
|
||||
+}
|
||||
+
|
||||
+func readEncryptionRequest(data []byte) (*encryptionRequestPacket, error) {
|
||||
+ r := bytes.NewReader(data)
|
||||
+ serverID, err := readString(r)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ pubKey, err := readByteArray(r)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ verifyToken, err := readByteArray(r)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ return &encryptionRequestPacket{
|
||||
+ ServerID: serverID,
|
||||
+ PublicKey: pubKey,
|
||||
+ VerifyToken: verifyToken,
|
||||
+ }, nil
|
||||
+}
|
||||
+
|
||||
+// Encryption Response packet
|
||||
+
|
||||
+type encryptionResponsePacket struct {
|
||||
+ SharedSecret []byte
|
||||
+ VerifyToken []byte
|
||||
+}
|
||||
+
|
||||
+func encodeEncryptionResponse(sharedSecret []byte, verifyToken []byte) []byte {
|
||||
+ var buf bytes.Buffer
|
||||
+ buf.Write(encodeByteArray(sharedSecret))
|
||||
+ buf.Write(encodeByteArray(verifyToken))
|
||||
+ return buf.Bytes()
|
||||
+}
|
||||
+
|
||||
+func readEncryptionResponse(data []byte) (*encryptionResponsePacket, error) {
|
||||
+ r := bytes.NewReader(data)
|
||||
+ secret, err := readByteArray(r)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ token, err := readByteArray(r)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ return &encryptionResponsePacket{
|
||||
+ SharedSecret: secret,
|
||||
+ VerifyToken: token,
|
||||
+ }, nil
|
||||
+}
|
||||
+
|
||||
+// Login Success packet
|
||||
+
|
||||
+func encodeLoginSuccess(name string) []byte {
|
||||
+ // Generate a deterministic UUID from username
|
||||
+ hash := sha1.Sum([]byte("OfflinePlayer:" + name))
|
||||
+ hash[6] = hash[6]&0x0f | 0x30 // version 3
|
||||
+ hash[8] = hash[8]&0x3f | 0x80 // variant 2
|
||||
+
|
||||
+ var buf bytes.Buffer
|
||||
+ buf.Write(hash[:16]) // UUID
|
||||
+ buf.Write(encodeString(name))
|
||||
+ buf.Write(encodeVarInt(0)) // Number Of Properties = 0
|
||||
+ buf.WriteByte(0x01) // Strict Error Handling = true
|
||||
+ return buf.Bytes()
|
||||
+}
|
||||
+
|
||||
+// Disconnect (Login) packet — plain text reason
|
||||
+func encodeLoginDisconnect(reason string) []byte {
|
||||
+ msg, _ := json.Marshal(map[string]string{"text": reason})
|
||||
+ return encodeString(string(msg))
|
||||
+}
|
||||
+
|
||||
+// Disconnect (Login) packet — translation key (e.g. multiplayer.disconnect.server_full)
|
||||
+func encodeLoginDisconnectTranslate(key string) []byte {
|
||||
+ msg, _ := json.Marshal(map[string]string{"translate": key})
|
||||
+ return encodeString(string(msg))
|
||||
+}
|
||||
+
|
||||
+// Status Response packet — mirrors real Vanilla server JSON format
|
||||
+
|
||||
+type statusResponse struct {
|
||||
+ Version *statusVersion `json:"version,omitempty"`
|
||||
+ Players *statusPlayers `json:"players,omitempty"`
|
||||
+ Description json.RawMessage `json:"description,omitempty"`
|
||||
+ Favicon string `json:"favicon,omitempty"`
|
||||
+ EnforcesSecureChat bool `json:"enforcesSecureChat,omitempty"`
|
||||
+}
|
||||
+
|
||||
+type statusVersion struct {
|
||||
+ Name string `json:"name"`
|
||||
+ Protocol int `json:"protocol"`
|
||||
+}
|
||||
+
|
||||
+type statusPlayers struct {
|
||||
+ Max int `json:"max"`
|
||||
+ Online int `json:"online"`
|
||||
+ Sample []statusPlayerSample `json:"sample,omitempty"`
|
||||
+}
|
||||
+
|
||||
+type statusPlayerSample struct {
|
||||
+ Name string `json:"name"`
|
||||
+ ID string `json:"id"`
|
||||
+}
|
||||
+
|
||||
+// encodeDescription converts a json.RawMessage description to the wire format.
|
||||
+// Accepts a JSON Chat Component (object/array), a JSON string, or null/empty.
|
||||
+// A JSON string like "hello" is unwrapped and re-wrapped as {"text": "hello"}.
|
||||
+func encodeDescription(desc json.RawMessage) json.RawMessage {
|
||||
+ if len(desc) == 0 || string(desc) == "null" {
|
||||
+ data, _ := json.Marshal(map[string]string{"text": "A Minecraft Server"})
|
||||
+ return data
|
||||
+ }
|
||||
+ // Already an object or array — use as-is
|
||||
+ if desc[0] == '{' || desc[0] == '[' {
|
||||
+ return desc
|
||||
+ }
|
||||
+ // JSON string value (e.g. "hello") — unwrap and wrap as {"text": "..."}
|
||||
+ if desc[0] == '"' {
|
||||
+ var s string
|
||||
+ if json.Unmarshal(desc, &s) == nil {
|
||||
+ data, _ := json.Marshal(map[string]string{"text": s})
|
||||
+ return data
|
||||
+ }
|
||||
+ }
|
||||
+ // Fallback
|
||||
+ data, _ := json.Marshal(map[string]string{"text": "A Minecraft Server"})
|
||||
+ return data
|
||||
+}
|
||||
+
|
||||
+func encodeStatusResponse(status *option.MinecraftStatus) []byte {
|
||||
+ resp := statusResponse{}
|
||||
+
|
||||
+ // Version
|
||||
+ if status.Version != nil {
|
||||
+ resp.Version = &statusVersion{
|
||||
+ Name: status.Version.Name,
|
||||
+ Protocol: status.Version.Protocol,
|
||||
+ }
|
||||
+ } else {
|
||||
+ resp.Version = &statusVersion{Name: versionName, Protocol: protocolVersion}
|
||||
+ }
|
||||
+
|
||||
+ // Players
|
||||
+ if status.Players != nil {
|
||||
+ players := &statusPlayers{
|
||||
+ Max: status.Players.Max,
|
||||
+ Online: status.Players.Online,
|
||||
+ }
|
||||
+ for _, s := range status.Players.Sample {
|
||||
+ id := s.ID
|
||||
+ if id == "" {
|
||||
+ hash := sha1.Sum([]byte("OfflinePlayer:" + s.Name))
|
||||
+ hash[6] = hash[6]&0x0f | 0x30
|
||||
+ hash[8] = hash[8]&0x3f | 0x80
|
||||
+ id = fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", hash[0:4], hash[4:6], hash[6:8], hash[8:10], hash[10:16])
|
||||
+ }
|
||||
+ players.Sample = append(players.Sample, statusPlayerSample{Name: s.Name, ID: id})
|
||||
+ }
|
||||
+ resp.Players = players
|
||||
+ } else {
|
||||
+ resp.Players = &statusPlayers{Max: 20, Online: 0}
|
||||
+ }
|
||||
+
|
||||
+ // Description (plain text or JSON Chat Component)
|
||||
+ resp.Description = encodeDescription(status.Description)
|
||||
+
|
||||
+ // Favicon
|
||||
+ resp.Favicon = status.Favicon
|
||||
+
|
||||
+ // EnforcesSecureChat
|
||||
+ resp.EnforcesSecureChat = status.EnforcesSecureChat
|
||||
+
|
||||
+ data, _ := json.Marshal(resp)
|
||||
+ return encodeString(string(data))
|
||||
+}
|
||||
+
|
||||
+// Legacy Server List Ping (pre-1.7)
|
||||
+// Detects 0xFE as first byte; responds with 0xFF kick containing server info
|
||||
+
|
||||
+const legacyPingByte = 0xFE
|
||||
+
|
||||
+// descriptionToPlainText extracts plain text from a json.RawMessage description.
|
||||
+// Used for legacy ping which only supports plain text.
|
||||
+func descriptionToPlainText(desc json.RawMessage) string {
|
||||
+ if len(desc) == 0 || string(desc) == "null" {
|
||||
+ return "A Minecraft Server"
|
||||
+ }
|
||||
+ // JSON string → unwrap
|
||||
+ if desc[0] == '"' {
|
||||
+ var s string
|
||||
+ if json.Unmarshal(desc, &s) == nil {
|
||||
+ return s
|
||||
+ }
|
||||
+ }
|
||||
+ // JSON object → extract "text" field
|
||||
+ if desc[0] == '{' {
|
||||
+ var obj struct {
|
||||
+ Text string `json:"text"`
|
||||
+ }
|
||||
+ if json.Unmarshal(desc, &obj) == nil && obj.Text != "" {
|
||||
+ return obj.Text
|
||||
+ }
|
||||
+ }
|
||||
+ return "A Minecraft Server"
|
||||
+}
|
||||
+
|
||||
+func encodeLegacyPingResponse(status *option.MinecraftStatus) []byte {
|
||||
+ ver := versionName
|
||||
+ proto := protocolVersion
|
||||
+ if status.Version != nil {
|
||||
+ if status.Version.Name != "" {
|
||||
+ ver = status.Version.Name
|
||||
+ }
|
||||
+ if status.Version.Protocol != 0 {
|
||||
+ proto = status.Version.Protocol
|
||||
+ }
|
||||
+ }
|
||||
+ motd := descriptionToPlainText(status.Description)
|
||||
+ var online, max int
|
||||
+ if status.Players != nil {
|
||||
+ online = status.Players.Online
|
||||
+ max = status.Players.Max
|
||||
+ } else {
|
||||
+ max = 20
|
||||
+ }
|
||||
+
|
||||
+ // Response format: 0xFF + string length (uint16 BE) + UTF-16BE string
|
||||
+ // String: "§1\0<protocol>\0<version>\0<motd>\0<online>\0<max>"
|
||||
+ payload := fmt.Sprintf("\u00a71\x00%d\x00%s\x00%s\x00%d\x00%d",
|
||||
+ proto, ver, motd, online, max)
|
||||
+ runes := []rune(payload)
|
||||
+
|
||||
+ // Build response: 0xFF + length (uint16 BE) + UTF-16BE chars
|
||||
+ var buf bytes.Buffer
|
||||
+ buf.WriteByte(0xFF)
|
||||
+ binary.Write(&buf, binary.BigEndian, uint16(len(runes)))
|
||||
+ for _, r := range runes {
|
||||
+ binary.Write(&buf, binary.BigEndian, uint16(r))
|
||||
+ }
|
||||
+ return buf.Bytes()
|
||||
+}
|
||||
+
|
||||
+// Shared secret derivation from password + verify token
|
||||
+
|
||||
+func deriveSharedSecret(password string, verifyToken []byte) []byte {
|
||||
+ h := sha256.New()
|
||||
+ h.Write([]byte(password))
|
||||
+ h.Write(verifyToken)
|
||||
+ sum := h.Sum(nil)
|
||||
+ return sum[:sharedSecretLen]
|
||||
+}
|
||||
+
|
||||
+// AES/CFB8 encrypted connection wrapper
|
||||
+// Minecraft uses CFB8 mode (1-byte segments), not standard CFB128
|
||||
+
|
||||
+type cfb8Cipher struct {
|
||||
+ block cipher.Block
|
||||
+ iv []byte
|
||||
+}
|
||||
+
|
||||
+func newCFB8Encrypt(block cipher.Block, iv []byte) *cfb8Cipher {
|
||||
+ ivCopy := make([]byte, len(iv))
|
||||
+ copy(ivCopy, iv)
|
||||
+ return &cfb8Cipher{block: block, iv: ivCopy}
|
||||
+}
|
||||
+
|
||||
+func newCFB8Decrypt(block cipher.Block, iv []byte) *cfb8Cipher {
|
||||
+ ivCopy := make([]byte, len(iv))
|
||||
+ copy(ivCopy, iv)
|
||||
+ return &cfb8Cipher{block: block, iv: ivCopy}
|
||||
+}
|
||||
+
|
||||
+func (c *cfb8Cipher) encrypt(dst, src []byte) {
|
||||
+ bs := c.block.BlockSize()
|
||||
+ tmp := make([]byte, bs)
|
||||
+ for i := range src {
|
||||
+ c.block.Encrypt(tmp, c.iv)
|
||||
+ dst[i] = src[i] ^ tmp[0]
|
||||
+ copy(c.iv, c.iv[1:])
|
||||
+ c.iv[bs-1] = dst[i]
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func (c *cfb8Cipher) decrypt(dst, src []byte) {
|
||||
+ bs := c.block.BlockSize()
|
||||
+ tmp := make([]byte, bs)
|
||||
+ for i := range src {
|
||||
+ c.block.Encrypt(tmp, c.iv)
|
||||
+ copy(c.iv, c.iv[1:])
|
||||
+ c.iv[bs-1] = src[i]
|
||||
+ dst[i] = src[i] ^ tmp[0]
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// encryptedConn wraps a net.Conn with Minecraft AES/CFB8 encryption
|
||||
+type encryptedConn struct {
|
||||
+ net.Conn
|
||||
+ enc *cfb8Cipher
|
||||
+ dec *cfb8Cipher
|
||||
+}
|
||||
+
|
||||
+func newEncryptedConn(conn net.Conn, sharedSecret []byte) (*encryptedConn, error) {
|
||||
+ block, err := aes.NewCipher(sharedSecret)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ // Minecraft uses the shared secret as both key and IV
|
||||
+ return &encryptedConn{
|
||||
+ Conn: conn,
|
||||
+ enc: newCFB8Encrypt(block, sharedSecret),
|
||||
+ dec: newCFB8Decrypt(block, sharedSecret),
|
||||
+ }, nil
|
||||
+}
|
||||
+
|
||||
+func (c *encryptedConn) Read(b []byte) (int, error) {
|
||||
+ n, err := c.Conn.Read(b)
|
||||
+ if n > 0 {
|
||||
+ c.dec.decrypt(b[:n], b[:n])
|
||||
+ }
|
||||
+ return n, err
|
||||
+}
|
||||
+
|
||||
+func (c *encryptedConn) Write(b []byte) (int, error) {
|
||||
+ encrypted := make([]byte, len(b))
|
||||
+ c.enc.encrypt(encrypted, b)
|
||||
+ return c.Conn.Write(encrypted)
|
||||
+}
|
||||
+
|
||||
+// RSA helpers
|
||||
+
|
||||
+func generateRSAKeyPair() (*rsa.PrivateKey, error) {
|
||||
+ return rsa.GenerateKey(rand.Reader, rsaKeyBits)
|
||||
+}
|
||||
+
|
||||
+func marshalPublicKey(pub *rsa.PublicKey) ([]byte, error) {
|
||||
+ return x509.MarshalPKIXPublicKey(pub)
|
||||
+}
|
||||
+
|
||||
+func parsePublicKey(data []byte) (*rsa.PublicKey, error) {
|
||||
+ pub, err := x509.ParsePKIXPublicKey(data)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ rsaPub, ok := pub.(*rsa.PublicKey)
|
||||
+ if !ok {
|
||||
+ return nil, fmt.Errorf("not an RSA public key")
|
||||
+ }
|
||||
+ return rsaPub, nil
|
||||
+}
|
||||
+
|
||||
+func generateVerifyToken() ([]byte, error) {
|
||||
+ token := make([]byte, verifyTokenLen)
|
||||
+ _, err := rand.Read(token)
|
||||
+ return token, err
|
||||
+}
|
||||
+
|
||||
+// smux config
|
||||
+
|
||||
+func smuxConfig() *smux.Config {
|
||||
+ config := smux.DefaultConfig()
|
||||
+ config.KeepAliveDisabled = true
|
||||
+ return config
|
||||
+}
|
||||
+
|
||||
+// Stream protocol constants (same as MySQL protocol)
|
||||
+const (
|
||||
+ commandTCP byte = 0x01
|
||||
+ commandUDP byte = 0x03
|
||||
+)
|
||||
Reference in New Issue
Block a user