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