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