463 lines
14 KiB
Diff
463 lines
14 KiB
Diff
--- /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) NewConnection(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)
|
|
+}
|