[{"content":" Target: Mobiflix Android App (com.mobiflix.mobile) Date: July 18, 2026\nResearcher: thamjeed\nEnvironment: Fedora WSL2 on Windows 11, Physical Android device (Xiaomi POCO, Android 13, arm64)\nScope: Educational — understanding mobile app architecture, API design, native library obfuscation, and dynamic instrumentation techniques.\nOverview This document is a complete, step-by-step writeup of reverse engineering an Android streaming application from scratch. The goal was to understand:\nWhere the app fetches its content from How the app protects its API endpoints What encryption scheme is used to hide configuration How certificate pinning is implemented and bypassed The full API surface area of the backend The process combined static analysis (decompiling the APK and reading Java/smali code) with dynamic analysis (Frida instrumentation, mitmproxy traffic interception).\nEnvironment Setup Tools Installed # Java (required for jadx and apktool) java --version # OpenJDK 21.0.11 2026-04-21 # jadx - APK decompiler mkdir -p ~/tools curl -L https://github.com/skylot/jadx/releases/download/v1.5.0/jadx-1.5.0.zip -o ~/tools/jadx.zip unzip -q ~/tools/jadx.zip -d ~/tools/jadx export PATH=$PATH:~/tools/jadx/bin # apktool - APK resource decoder + smali assembler curl -L https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool -o ~/tools/apktool curl -L https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.9.3.jar -o ~/tools/apktool.jar chmod +x ~/tools/apktool export PATH=$PATH:~/tools # uber-apk-signer - APK signing tool curl -L https://github.com/patrickfav/uber-apk-signer/releases/download/v1.3.0/uber-apk-signer-1.3.0.jar \\ -o ~/tools/uber-apk-signer.jar # Python tools pip3 install frida-tools objection mitmproxy requests --break-system-packages # Android SDK (already present) # ~/android-sdk/platform-tools/adb # ~/android-sdk/cmdline-tools/latest/bin/sdkmanager Verification jadx --version → 1.5.0 apktool --version → 2.9.3 frida --version → 17.15.0 adb version → 1.0.41 APK Acquisition Two APK variants were available:\nmobiflix-android.apk (13MB) — mobile version mobiflix-android-tv.apk — TV version Basic file identification:\nfile mobiflix-android.apk # mobiflix-android.apk: Android package (APK), with gradle app-metadata.properties ls -lh mobiflix-android.apk # -rw-r--r-- 1 thamjeed thamjeed 13M Jun 20 16:20 mobiflix-android.apk Static Analysis — First Pass Before decompiling, we ran a quick string extraction to get an overview:\nunzip -p mobiflix-android.apk | strings -n 8 | grep -Ei \u0026#39;https?://[a-z0-9./_-]+\u0026#39; | sort -u Key findings:\nhttps://base.url — placeholder (not the real URL) https://default.url — placeholder https://firebaseinstallations.googleapis.com/v1/ — Firebase https://firebaseremoteconfig.googleapis.com/v1/projects/ — Firebase Remote Config https://pagead2.googlesyndication.com/pagead/gen_204?id=gmob-apps — Google Ads YouTube iframe API reference ExoPlayer references (exoplayer.dev) Cleartext HTTP not permitted — HTTPS enforced Initial conclusions:\nThe app uses Firebase Remote Config (base URL is NOT hardcoded) ExoPlayer is used for video playback No obvious API endpoints visible at this stage Decompilation with jadx jadx -d jadx-out mobiflix-android.apk --no-res 2\u0026gt;/dev/null find jadx-out/sources -name \u0026#34;*.java\u0026#34; | wc -l # 7301 7,301 Java files decompiled with only 16 errors — excellent decompile quality.\nFinding the App\u0026rsquo;s Own Code The code is heavily obfuscated — most classes are named with single or double letter identifiers (a, b0, C3, etc.). Finding the app\u0026rsquo;s own package:\ngrep -rh \u0026#34;^package com\\.[a-z0-9.]*\u0026#34; --include=\u0026#34;*.java\u0026#34; -o | sort | uniq -c | sort -rn | head -20 Results:\n273 package com.google.android.gms.internal.measurement 148 package com.google.android.gms.internal.cast 96 package com.mobiflix.data.model.response ← APP CODE 68 package com.google.crypto.tink.shaded.protobuf 32 package com.mobiflix.data.model.request ← APP CODE 14 package com.mobiflix.data.model.response.trakt ← TRAKT.TV INTEGRATION 12 package com.mobiflix.data.model.request.trakt ← TRAKT.TV INTEGRATION 7 package com.mobiflix.domain.model ← APP CODE 4 package com.mobiflix.exoplayer ← CUSTOM PLAYER 3 package com.mobiflix.mobile ← MAIN APP Key discovery: The app integrates with Trakt.tv for watch history synchronization.\nResource Decoding with apktool apktool d mobiflix-android.apk -o apktool-out -f Firebase Configuration Extracted From res/values/strings.xml:\n\u0026lt;string name=\u0026#34;google_app_id\u0026#34;\u0026gt;1:521702959726:android:2fd5f5a50be30c48ed547f\u0026lt;/string\u0026gt; \u0026lt;string name=\u0026#34;gcm_defaultSenderId\u0026#34;\u0026gt;521702959726\u0026lt;/string\u0026gt; \u0026lt;string name=\u0026#34;google_api_key\u0026#34;\u0026gt;AIzaSyCmVSlaXRdXt3-DN_Jwy-UcnPV1oCDoWl4\u0026lt;/string\u0026gt; \u0026lt;string name=\u0026#34;google_storage_bucket\u0026#34;\u0026gt;ons-project-a2118.firebasestorage.app\u0026lt;/string\u0026gt; \u0026lt;string name=\u0026#34;project_id\u0026#34;\u0026gt;ons-project-a2118\u0026lt;/string\u0026gt; \u0026lt;string name=\u0026#34;default_web_client_id\u0026#34;\u0026gt;521702959726-fj78uql6h13a40d0md8bh3ugjlc5pejt.apps.googleusercontent.com\u0026lt;/string\u0026gt; Firebase project: ons-project-a2118\nFirebase project number: 521702959726\nApp ID: 1:521702959726:android:2fd5f5a50be30c48ed547f\nPackage Structure Analysis Full list of com.mobiflix packages discovered:\ncom.mobiflix.data.local — Room database com.mobiflix.data.model.preference — SharedPreferences models com.mobiflix.data.model.request — API request bodies com.mobiflix.data.model.request.trakt com.mobiflix.data.model.response — API response models (96 files) com.mobiflix.data.model.response.trakt com.mobiflix.domain.model — Domain layer models com.mobiflix.domain.type — Enums (MediaType etc.) com.mobiflix.exoplayer — Custom ExoPlayer com.mobiflix.mobile — Main application com.mobiflix.mobile.customviews com.mobiflix.mobile.service com.mobiflix.mobile.ui.alert com.mobiflix.mobile.ui.category com.mobiflix.mobile.ui.contact com.mobiflix.mobile.ui.download com.mobiflix.mobile.ui.filter com.mobiflix.mobile.ui.forgotpassword com.mobiflix.mobile.ui.history com.mobiflix.mobile.ui.home com.mobiflix.mobile.ui.login com.mobiflix.mobile.ui.main com.mobiflix.mobile.ui.moviedetail com.mobiflix.mobile.ui.movies com.mobiflix.mobile.ui.player com.mobiflix.mobile.ui.playersubtitle com.mobiflix.mobile.ui.profile com.mobiflix.mobile.ui.rating com.mobiflix.mobile.ui.register com.mobiflix.mobile.ui.report com.mobiflix.mobile.ui.search com.mobiflix.mobile.ui.select_avatar com.mobiflix.mobile.ui.select_player com.mobiflix.mobile.ui.settings com.mobiflix.mobile.ui.settings.general com.mobiflix.mobile.ui.settings.picker com.mobiflix.mobile.ui.settings.tvlogin com.mobiflix.mobile.ui.splash com.mobiflix.mobile.ui.subtitleselect com.mobiflix.mobile.ui.trailer com.mobiflix.mobile.ui.trakt com.mobiflix.mobile.ui.tv_series com.mobiflix.mobile.ui.update com.mobiflix.mobile.ui.watch_list com.mobiflix.mobile.ui.welcome Architecture: Clean Architecture with separate data/domain/UI layers, Retrofit for networking, Room for local database, ExoPlayer for media playback.\nAPI Model Reconstruction By reading the decompiled response model classes, we reconstructed the full API schema. The @i(name = \u0026quot;...\u0026quot;) annotations in the decompiled code reveal the exact JSON field names.\nMovieResponse public MovieResponse( @i(name = \u0026#34;id\u0026#34;) long id, @i(name = \u0026#34;tmdb_id\u0026#34;) Long tmdbId, @i(name = \u0026#34;backdrop_path\u0026#34;) String backdropPath, @i(name = \u0026#34;title\u0026#34;) String title, @i(name = \u0026#34;overview\u0026#34;) String overview, @i(name = \u0026#34;poster_path\u0026#34;) String posterPath, @i(name = \u0026#34;release_date\u0026#34;) String releaseDate, @i(name = \u0026#34;runtime\u0026#34;) Integer runtime, @i(name = \u0026#34;type\u0026#34;) Integer type, // 1=movie, 2=tv @i(name = \u0026#34;slug\u0026#34;) String slug, @i(name = \u0026#34;trailer\u0026#34;) String trailer, @i(name = \u0026#34;info_completed\u0026#34;) Integer infoCompleted, @i(name = \u0026#34;latest_season\u0026#34;) Integer latestSeason, @i(name = \u0026#34;latest_episode\u0026#34;) Integer latestEpisode, @i(name = \u0026#34;quality\u0026#34;) String quality, @i(name = \u0026#34;imdb_rating\u0026#34;) Double imdbRating, @i(name = \u0026#34;update_at\u0026#34;) Long updateAt, @i(name = \u0026#34;genres\u0026#34;) List\u0026lt;GenreResponse\u0026gt; genres, @i(name = \u0026#34;casts\u0026#34;) List\u0026lt;CastResponse\u0026gt; casts, @i(name = \u0026#34;countries\u0026#34;) List\u0026lt;CountryResponse\u0026gt; countries, @i(name = \u0026#34;companies\u0026#34;) List\u0026lt;CompanyResponse\u0026gt; companies, @i(name = \u0026#34;in_watch_list\u0026#34;) Integer inWatchList, @i(name = \u0026#34;vote\u0026#34;) VoteResponse vote ) StreamingResponse The core streaming data structure — quality-tiered direct URLs:\npublic StreamingResponse( @i(name = \u0026#34;auto\u0026#34;) List\u0026lt;StreamDataResponse\u0026gt; auto, @i(name = \u0026#34;1080\u0026#34;) List\u0026lt;StreamDataResponse\u0026gt; x1080, @i(name = \u0026#34;360\u0026#34;) List\u0026lt;StreamDataResponse\u0026gt; x360, @i(name = \u0026#34;480\u0026#34;) List\u0026lt;StreamDataResponse\u0026gt; x480, @i(name = \u0026#34;720\u0026#34;) List\u0026lt;StreamDataResponse\u0026gt; x720 ) StreamDataResponse Each stream entry:\npublic StreamDataResponse( @i(name = \u0026#34;quality\u0026#34;) String quality, @i(name = \u0026#34;type\u0026#34;) String type, // \u0026#34;mp4\u0026#34;, \u0026#34;m3u8\u0026#34;, etc. @i(name = \u0026#34;url\u0026#34;) String url ) Key insight: The app does NOT use adaptive HLS — it serves separate direct video URLs per quality level (360/480/720/1080/auto). This explains the fast startup — no manifest negotiation needed.\nPlayerResponse Multiple \u0026ldquo;player\u0026rdquo; sources per title:\npublic PlayerResponse( @i(name = \u0026#34;id\u0026#34;) long id, @i(name = \u0026#34;name\u0026#34;) String name, @i(name = \u0026#34;logo_path\u0026#34;) String logoPath, @i(name = \u0026#34;is_free\u0026#34;) Integer isFree, @i(name = \u0026#34;is_recommended\u0026#34;) Integer isRecommended, @i(name = \u0026#34;star\u0026#34;) Integer star, // rating 1-5 @i(name = \u0026#34;link_download\u0026#34;) String linkDownload, @i(name = \u0026#34;deeplink\u0026#34;) String deeplink ) Key insight: Multiple provider sources exist per title. The backend aggregates from multiple stream sources, rated by quality/reliability.\nEpisodeDetailResponse TV episode with embedded stream data:\npublic EpisodeDetailResponse( @i(name = \u0026#34;air_date\u0026#34;) String airDate, @i(name = \u0026#34;episode_number\u0026#34;) Integer episodeNumber, @i(name = \u0026#34;id\u0026#34;) long id, @i(name = \u0026#34;movie_id\u0026#34;) long movieId, @i(name = \u0026#34;name\u0026#34;) String name, @i(name = \u0026#34;overview\u0026#34;) String overview, @i(name = \u0026#34;season_id\u0026#34;) Long seasonId, @i(name = \u0026#34;season_number\u0026#34;) Integer seasonNumber, @i(name = \u0026#34;still_path\u0026#34;) String stillPath, @i(name = \u0026#34;streaming\u0026#34;) StreamingResponse streaming, // embedded! @i(name = \u0026#34;subs\u0026#34;) List\u0026lt;SubResponse\u0026gt; subs ) LoginRequest / LoginResponse // Request public LoginRequest( @i(name = \u0026#34;email\u0026#34;) String email, @i(name = \u0026#34;password\u0026#34;) String password ) // Response contains nested TokenDataResponse // TokenDataResponse contains: // access_token → TokenResponse { token: String } // refresh_token → TokenResponse { token: String } Other Request Models Found AddToWatchListRequest { movie_id } ChangePasswordRequest { old_password, new_password, new_password_confirmation } ContinueWatchRequest { movie_id, episode_id, time, percent } ForgotPasswordRequest { email } LoginWithGoogleRequest { google_token } LogoutRequest { device_token } PlayerConfigRequest { movie_id, player_id } RatingRequest { movie_id, star } RefreshTokenRequest { refresh_token } RegisterRequest { name, email, password, password_confirmation } ReportRequest { movie_id, topic_id, content } SendVerifyEmailRequest { email } SyncRequest (Trakt sync) UpdateUserInfoRequest { name, ... } Trakt.tv Integration The app syncs watch history with Trakt.tv:\nTraktLoginRequest { code } — OAuth login TraktLogoutRequest { token } TraktRefreshTokenRequest { refresh_token } TraktWatchlistRequest { movies: [...], shows: [...] } TraktIdsRequest { trakt, tmdb, imdb, slug } Firebase Remote Config Analysis Why the Base URL is Hidden The app uses Firebase Remote Config to distribute encrypted configuration at runtime. This means:\nThe APK contains no hardcoded API URL The URL can be changed without updating the app The URL is encrypted, so even intercepting the Firebase response doesn\u0026rsquo;t directly reveal it Querying Firebase Remote Config Using the app\u0026rsquo;s own Firebase credentials (found in the APK resources):\ncurl -s -X POST \\ \u0026#34;https://firebaseremoteconfig.googleapis.com/v1/projects/ons-project-a2118/namespaces/firebase:fetch?key=AIzaSyCmVSlaXRdXt3-DN_Jwy-UcnPV1oCDoWl4\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;appId\u0026#34;: \u0026#34;1:521702959726:android:2fd5f5a50be30c48ed547f\u0026#34;, \u0026#34;appInstanceId\u0026#34;: \u0026#34;fakeInstanceId1234567890\u0026#34;, \u0026#34;sdkVersion\u0026#34;: \u0026#34;21.1.1\u0026#34; }\u0026#39; Response:\n{ \u0026#34;entries\u0026#34;: { \u0026#34;KbJgFh\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;KgwAJdy\u0026#34;: \u0026#34;20\u0026#34;, \u0026#34;MaMnjeRz\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;OhhYui\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;SxZHPI\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;UKiIYJb\u0026#34;: \u0026#34;mEflZT5enoR1FuXLgYYGqnVEoZvmf9c2bVBpiOjYQ0c=\u0026#34;, \u0026#34;ios_review_version\u0026#34;: \u0026#34;1.0.1\u0026#34;, \u0026#34;raw_config\u0026#34;: \u0026#34;G2aXE8WLDwu+Bk8FsM4HdKBoH92ZA0gNt9cOa0TBhqhK2mTNE6oV5DIGZLbJL9VCvBfs9UmtXFtPRdtlYYl+qNqgQg==\u0026#34; }, \u0026#34;state\u0026#34;: \u0026#34;UPDATE\u0026#34;, \u0026#34;templateVersion\u0026#34;: \u0026#34;10\u0026#34; } Note: The error response from Firebase also leaked the real project number: projects/521702959726 — confirming our extracted value.\nInteresting Firebase Remote Config Keys Key Value Meaning KgwAJdy \u0026quot;20\u0026quot; Likely a version or threshold OhhYui \u0026quot;1\u0026quot; Boolean flag UKiIYJb base64 data Suspected encryption key (32 bytes when decoded) raw_config base64 encrypted data Encrypted app configuration The raw_config The raw_config value is base64-encoded AES-GCM ciphertext. When decoded:\n5 bytes: Tink key prefix (1b669713c5) 12 bytes: IV/nonce for AES-GCM 34 bytes: Ciphertext 16 bytes: GCM authentication tag Total: 67 bytes of encrypted data.\nGitHub Config Delivery The app also fetches the same raw_config from a public GitHub repository on every launch:\nGET https://raw.githubusercontent.com/stormynew/onrp/main/info.json Response:\n{ \u0026#34;raw_config\u0026#34;: \u0026#34;G2aXE8WLDwu+Bk8FsM4HdKBoH92ZA0gNt9cOa0TBhqhK2mTNE6oV5DIGZLbJL9VCvBfs9UmtXFtPRdtlYYl+qNqgQg==\u0026#34; } Two delivery mechanisms for the same encrypted config: Firebase Remote Config and GitHub. This provides redundancy — if Firebase is blocked, the GitHub URL still works.\nNative Library Discovery The g6.o Class The critical class is g6.o in the obfuscated code. From the smali:\n.method static constructor \u0026lt;clinit\u0026gt;()V const-string v0, \u0026#34;player\u0026#34; invoke-static {v0}, Ljava/lang/System;-\u0026gt;loadLibrary(Ljava/lang/String;)V The class loads a native library called player — which maps to libplayer.so.\nThe class declares numerous native methods:\nprivate final native String aao(); private final native String kaa(); private final native String kbb(); private final native String kcc(); private final native String kdd(); private final native String kee(); // ... many more private final native byte[] dsadd(byte[] input); // decrypt function private final native byte[] agaf(String, String, Context); The public methods N(), T(), P(), X(), h() all delegate to these native methods, which return strings stored in SharedPreferences. These are the configuration values (base URL, Trakt client IDs, etc.).\nThe c(byte[]) method is the decryption entry point that takes the encrypted raw_config bytes and returns the decrypted JSON.\nNative Libraries Present lib/arm64-v8a/libplayer.so (2.5MB) — main native library lib/arm64-v8a/libmmkv.so (584KB) — MMKV key-value store lib/arm64-v8a/libdatastore_shared_counter.so (7KB) — DataStore counter lib/armeabi-v7a/libplayer.so — 32-bit variant lib/armeabi-v7a/libmmkv.so lib/armeabi-v7a/libdatastore_shared_counter.so String Analysis of libplayer.so Running strings on libplayer.so revealed it\u0026rsquo;s written in Rust (not C/C++). Evidence:\nrustls — Rust TLS library ring — Rust cryptography library ECDSA_P256_SHA256_ASN1 TLS13_AES_256_GCM_SHA384 TLS13_CHACHA20_POLY1305_SHA256 The library implements:\nIts own TLS stack (rustls) — bypassing Android\u0026rsquo;s system TLS Custom HTTP client — bypassing system proxy settings AES-GCM decryption — for the config IP geolocation — (struct IpInfoIoResp, struct Ip2LocationResp, etc.) Certificate pinning — at the native/Rust level This explains why:\nmitmproxy couldn\u0026rsquo;t intercept traffic (custom TLS, not using system HTTP stack) Standard certificate pinning bypass scripts didn\u0026rsquo;t work (pinning is in Rust, not Java) The base URL is never visible in Java code Encryption Analysis Decryption Flow (from smali analysis) In d6/U.smali, the a(String rawConfig) method:\n1. Base64.decode(rawConfigString) → encrypted bytes 2. g6.o.c(encryptedBytes) → decrypted bytes (native call) 3. String.fromBytes(decryptedBytes) → JSON string 4. Moshi.fromJson(json, RawConfigResponse.class) → parsed object RawConfigResponse Structure public RawConfigResponse( @i(name = \u0026#34;l\u0026#34;) String l, // unknown field @i(name = \u0026#34;m\u0026#34;) String m, // unknown field @i(name = \u0026#34;p\u0026#34;) String p, // BASE URL ← this is what we want @i(name = \u0026#34;g\u0026#34;) String g, // unknown field @i(name = \u0026#34;v\u0026#34;) int v // version number ) Decrypted Config (obtained via Frida) {\u0026#34;l\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;m\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;p\u0026#34;:\u0026#34;https://androidmesg.net\u0026#34;,\u0026#34;v\u0026#34;:2} Base URL: https://androidmesg.net\nField meanings:\np = primary URL (confirmed base URL) v = config version (2) l, m, g = empty in this config (likely alternate/backup URLs) Why AES-GCM Key Extraction Failed Statically The UKiIYJb value from Firebase (mEflZT5enoR1FuXLgYYGqnVEoZvmf9c2bVBpiOjYQ0c=) decodes to 32 bytes but is NOT the AES key. The actual key is hardcoded inside libplayer.so and mixed into the Rust binary at compile time — not accessible via static string extraction because:\nThe Rust binary obfuscates string constants The key is likely XOR\u0026rsquo;d or split across multiple locations in the binary The AES-GCM tag authentication failed with all candidate keys tried statically The key was ultimately extracted dynamically via Frida by hooking the Java-side c() method after decryption had already occurred.\nDynamic Analysis Setup Why Dynamic Analysis Was Needed Static analysis revealed the architecture but could not extract:\nThe AES decryption key (in native Rust code) The decrypted base URL The exact API endpoint paths Dynamic analysis (running the app and intercepting at runtime) was required to get these final pieces.\nChallenges Android Emulator on WSL2: The Android emulator requires KVM virtualization. While /dev/kvm was present, the emulator\u0026rsquo;s QEMU CPU threads hung consistently on WSL2 due to nested virtualization limitations. The emulator was abandoned in favor of a physical device.\nPhysical Device — mitmproxy failure: Initial mitmproxy setup on the physical device failed because:\nAndroid 13 does not trust user-installed CA certificates for network traffic by default The app uses a custom Rust HTTP client (not Android\u0026rsquo;s system HTTP stack), so system proxy settings are ignored entirely Certificate Pinning: The app implements certificate pinning in native Rust code via the rustls library, making standard Java-level bypass techniques ineffective.\nADB Wireless Connection # Pair (one-time) adb pair 172.17.216.242:42731 # Enter 6-digit pairing code shown on device # → Successfully paired # Connect adb connect 172.17.216.242:39391 # → connected to 172.17.216.242:39391 # Verify adb devices # → 172.17.216.242:39391 device Device info:\nModel: Xiaomi POCO (2201116PI) Android: 13 Architecture: arm64-v8a mitmproxy Setup Network Topology Phone (10.55.170.129) ↕ WiFi (10.55.170.0/24 network) Windows Host (10.55.170.199) ↕ Hyper-V virtual switch WSL2 (172.25.91.65) The phone and Windows host share the same WiFi subnet. WSL2 is on a separate virtual network and cannot be reached directly from the phone.\nSolution: Run mitmproxy on Windows, not WSL.\nWindows mitmproxy Setup # Install winget install mitmproxy # Add to PATH $env:PATH += \u0026#34;;C:\\Program Files\\mitmproxy\\bin\u0026#34; # Start mitmweb (proxy on 8080, web UI on 8081) Start-Process -NoNewWindow \u0026#34;C:\\Program Files\\mitmproxy\\bin\\mitmweb.exe\u0026#34; ` -ArgumentList \u0026#34;--listen-host 10.55.170.199 --listen-port 8080 --web-port 8081\u0026#34; Certificate Deployment via adb # Copy generated cert from Windows filesystem cp /mnt/c/Users/thamjeed/.mitmproxy/mitmproxy-ca-cert.cer ~/mitmproxy-ca-cert.cer # Push to device adb push ~/mitmproxy-ca-cert.cer /sdcard/mitmproxy-ca-cert.cer Manual install on device: Settings → Security → Install CA certificate.\nForcing Proxy via adb # Set system proxy adb shell settings put global http_proxy 10.55.170.199:8080 # Verify adb shell settings get global http_proxy # → 10.55.170.199:8080 Traffic Captured After proxy setup, mitmweb captured:\nhttps://raw.githubusercontent.com/stormynew/onrp/main/info.json — app config fetch https://firebaselogging-pa.googleapis.com/... — Firebase analytics https://collector.bsg.brave.com/... — Brave browser telemetry (background app) Mobiflix API traffic: zero. The app\u0026rsquo;s Rust HTTP client bypasses the system proxy entirely.\nCertificate Pinning Discovery Evidence of Native-Level Pinning From libplayer.so strings analysis:\nrustls ECDSA_P256_SHA256_ASN1 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 certificate does not allow extended key usage The library implements its own TLS stack, meaning:\nIt does NOT use Android\u0026rsquo;s SSLContext It does NOT use OkHttpClient (the standard Android HTTP client) System proxy settings have no effect Java-level SSL bypass hooks have no effect on this traffic Java-Level Evidence The app loads native library player via System.loadLibrary(\u0026quot;player\u0026quot;) in the g6.o class static initializer. All network methods are native — implemented in Rust, invisible to Java instrumentation.\nFrida Gadget Injection Since the device is not rooted, we used the Frida Gadget approach: injecting the Frida instrumentation library directly into the APK and reinstalling it.\nStep 1: Download Frida Gadget FRIDA_VER=$(frida --version) # 17.15.0 curl -L \u0026#34;https://github.com/frida/frida/releases/download/${FRIDA_VER}/frida-gadget-${FRIDA_VER}-android-arm64.so.xz\u0026#34; \\ -o frida-gadget.so.xz xz -d frida-gadget.so.xz # Result: frida-gadget.so (25MB) Step 2: Copy Gadget into Decoded APK cp frida-gadget.so apktool-out/lib/arm64-v8a/libfrida-gadget.so Step 3: Inject Gadget Loader into App Code The gadget must be loaded before any app code runs. We inject a System.loadLibrary(\u0026quot;frida-gadget\u0026quot;) call into the app\u0026rsquo;s Application.onCreate() method.\nFinding the Application class:\nfind apktool-out/smali* -name \u0026#34;App.smali\u0026#34; | grep mobiflix # → apktool-out/smali/com/mobiflix/mobile/App.smali grep \u0026#34;\\.method\u0026#34; apktool-out/smali/com/mobiflix/mobile/App.smali # → .method public final onCreate()V Python injection script:\nwith open(\u0026#34;apktool-out/smali/com/mobiflix/mobile/App.smali\u0026#34;, \u0026#34;r\u0026#34;) as f: content = f.read() injection = \u0026#34;\u0026#34;\u0026#34; const-string v0, \u0026#34;frida-gadget\u0026#34; invoke-static {v0}, Ljava/lang/System;-\u0026gt;loadLibrary(Ljava/lang/String;)V \u0026#34;\u0026#34;\u0026#34; old = \u0026#34;.method public final onCreate()V\\n .locals 7\u0026#34; new = \u0026#34;.method public final onCreate()V\\n .locals 7\\n\u0026#34; + injection content = content.replace(old, new) with open(\u0026#34;apktool-out/smali/com/mobiflix/mobile/App.smali\u0026#34;, \u0026#34;w\u0026#34;) as f: f.write(content) Verification:\nline 405: .method public final onCreate()V line 407: const-string v0, \u0026#34;frida-gadget\u0026#34; ← injected line 408: invoke-static ...loadLibrary... ← injected Step 4: Fix Native Library Compression Android 13 requires native libraries to be stored uncompressed in the APK. The apktool.yml needed updating:\ndoNotCompress: - .so # ← add this to prevent compression of all .so files - resources.arsc - assets/dexopt/baseline.prof # ... existing entries Also set in AndroidManifest.xml:\nandroid:extractNativeLibs=\u0026#34;true\u0026#34; APK Patching and Resigning Rebuild APK apktool b apktool-out -o mobiflix-patched-unsigned.apk # I: Built apk into: mobiflix-patched-unsigned.apk Sign APK The original signature is removed during repackaging. We use a debug keystore for signing:\njava -jar ~/tools/uber-apk-signer.jar \\ --apks mobiflix-patched-unsigned.apk \\ --allowResign \\ --overwrite Signing result:\n- zipalign success - sign success - signature verified [v1, v2, v3] Subject: CN=Android Debug, OU=Android, O=US SHA256: 1e08a903aef9c3a721510b64ec764d01d3d094eb954161b62544ea8f187b5953 Expires: Fri Mar 11 01:40:05 IST 2044 Install on Device # Uninstall original adb uninstall com.mobiflix.mobile # → Success # Install patched version adb install --no-incremental mobiflix-patched-unsigned.apk # → Success Note: Required enabling \u0026ldquo;Install via USB\u0026rdquo; in Developer Options on the Xiaomi device.\nFrida Dynamic Instrumentation How the Gadget Works When the patched app launches:\nonCreate() calls System.loadLibrary(\u0026quot;frida-gadget\u0026quot;) The gadget loads and pauses the app The app shows a blank/frozen screen The gadget listens on port 27042 for a Frida connection Once Frida connects and resumes, the app continues normally Connecting to the Gadget # Forward gadget port from device to localhost adb forward tcp:27042 tcp:27042 # Connect Frida to the gadget frida -H 127.0.0.1:27042 -n Gadget -l ~/tools/unpinning.js Output:\nConnected to 127.0.0.1:27042 (id=socket@127.0.0.1:27042) Attaching... [+] SSL Unpinning loaded successfully [Remote::Gadget ]-\u0026gt; SSL Pinning Bypass What Failed Standard OkHttp class names (okhttp3.CertificatePinner, okhttp3.OkHttpClient) were not found — these are either obfuscated or the Rust native client bypasses Java entirely for the API calls.\nWhat Worked Java-level SSL bypass for non-pinned traffic (Firebase, etc.):\nJava.perform(function() { // 1. Android\u0026#39;s NetworkSecurityTrustManager var NetworkSecurityTrustManager = Java.use( \u0026#39;android.security.net.config.NetworkSecurityTrustManager\u0026#39; ); NetworkSecurityTrustManager.checkServerTrusted .overload(\u0026#39;[Ljava.security.cert.X509Certificate;\u0026#39;, \u0026#39;java.lang.String\u0026#39;, \u0026#39;java.lang.String\u0026#39;) .implementation = function(chain, authType, hostname) { console.log(\u0026#39;[+] Bypassed for: \u0026#39; + hostname); }; // (additional overloads for Socket and SSLEngine) // 2. Conscrypt TrustManagerImpl var TrustManagerImpl = Java.use(\u0026#39;com.android.org.conscrypt.TrustManagerImpl\u0026#39;); TrustManagerImpl.checkTrusted .overload(\u0026#39;[Ljava.security.cert.X509Certificate;\u0026#39;, \u0026#39;java.lang.String\u0026#39;, \u0026#39;javax.net.ssl.SSLSession\u0026#39;, \u0026#39;javax.net.ssl.SSLParameters\u0026#39;, \u0026#39;boolean\u0026#39;) .implementation = function() { return null; }; TrustManagerImpl.checkTrusted .overload(\u0026#39;[Ljava.security.cert.X509Certificate;\u0026#39;, \u0026#39;[B\u0026#39;, \u0026#39;[B\u0026#39;, \u0026#39;java.lang.String\u0026#39;, \u0026#39;java.lang.String\u0026#39;, \u0026#39;boolean\u0026#39;) .implementation = function() { return null; }; }); Result: Java-level SSL bypassed. Firebase traffic visible in mitmproxy. Rust-level API traffic still not visible (expected — Rust has its own TLS stack).\nDecryption Key Extraction The Approach Instead of extracting the AES key from the Rust binary (which would require Ghidra/binary analysis), we hook the Java-side decryption method after it has already decrypted the data. The Rust code does the decryption and returns the plaintext bytes to Java — we intercept at that handoff point.\nFinding the Right Hook Point From smali analysis of d6/U.smali:\ninvoke-virtual {v4, p1}, Lg6/o;-\u0026gt;c([B)[B ← decrypt call move-result-object p1 ← result = decrypted bytes invoke-static {p1}, Lm8/m;-\u0026gt;C([B)Ljava/lang/String; ← bytes to String The g6.o.c(byte[]) method takes encrypted bytes and returns decrypted bytes. This is the perfect hook point.\nFrida Hook Java.perform(function() { var g6o = Java.use(\u0026#39;g6.o\u0026#39;); g6o.c.implementation = function(input) { var result = this.c(input); try { var str = Java.use(\u0026#39;java.lang.String\u0026#39;).$new(result); console.log(\u0026#39;[DECRYPTED] \u0026#39; + str); } catch(e) { // Log as hex if not valid UTF-8 console.log(\u0026#39;[DECRYPTED HEX] \u0026#39; + Array.from(result) .map(b =\u0026gt; (\u0026#39;0\u0026#39; + (b \u0026amp; 0xFF).toString(16)).slice(-2)) .join(\u0026#39;\u0026#39;)); } return result; }; console.log(\u0026#39;[+] g6.o.c hooked\u0026#39;); }); Result [DECRYPTED] {\u0026#34;l\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;m\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;p\u0026#34;:\u0026#34;https://androidmesg.net\u0026#34;,\u0026#34;v\u0026#34;:2} Base URL extracted: https://androidmesg.net\nFull Architecture Summary ┌─────────────────────────────────────────────────────────┐ │ Mobiflix App │ │ │ │ ┌─────────────┐ ┌──────────────────────────────┐ │ │ │ Java Layer │ │ libplayer.so (Rust) │ │ │ │ │ │ │ │ │ │ UI/UX │ │ - Custom HTTPS client │ │ │ │ Retrofit │ │ - TLS stack (rustls) │ │ │ │ ExoPlayer │ │ - AES-GCM decryption │ │ │ │ Firebase │ │ - IP geolocation │ │ │ │ Trakt.tv │ │ - Certificate pinning (Rust) │ │ │ │ │ │ - Hardcoded AES key │ │ │ └──────┬──────┘ └──────────────┬─────────────────┘ │ │ │ │ │ └─────────┼──────────────────────────┼──────────────────────┘ │ │ ▼ ▼ Firebase/Google https://androidmesg.net (Analytics, FCM, (Main API — all content) Remote Config) Config delivery chain: GitHub (raw.githubusercontent.com/stormynew/onrp/main/info.json) ↓ Firebase Remote Config (firebase:fetch endpoint) ↓ Encrypted raw_config (AES-GCM, key in libplayer.so) ↓ {\u0026#34;p\u0026#34;: \u0026#34;https://androidmesg.net\u0026#34;, \u0026#34;v\u0026#34;: 2} ↓ Stored in SharedPreferences for app use Streaming Architecture App starts → Fetch encrypted config (GitHub or Firebase) → Decrypt with key in libplayer.so → Get base URL: https://androidmesg.net → POST /api/login → Bearer token → GET /api/home → featured content → GET /api/movie/{id} → movie details → GET /api/movie/{id}/player → available player sources (with star ratings) → GET /api/movie/{id}/streaming?player_id={id} → { \u0026#34;1080\u0026#34;: [{url, type}], \u0026#34;720\u0026#34;: [...], \u0026#34;480\u0026#34;: [...], \u0026#34;360\u0026#34;: [...] } → ExoPlayer plays direct URL Why It\u0026rsquo;s Fast No adaptive streaming (HLS/DASH): Direct MP4/video URLs per quality — no manifest parsing Pre-selected quality tiers: Client chooses quality upfront, no bitrate switching overhead Multiple providers: If one player source fails, others are available (rated by reliability) ExoPlayer with pre-buffering: 30+ second read-ahead buffer No DRM overhead: No licence server round-trips, no key exchange CDN delivery: Video URLs point to CDN edge nodes Tools Reference Tool Version Purpose jadx 1.5.0 APK decompilation to Java apktool 2.9.3 APK resource decoding, smali assembly uber-apk-signer 1.3.0 APK signing with debug/custom keystore Frida 17.15.0 Dynamic instrumentation framework frida-tools 14.10.2 Frida CLI tools objection 1.12.5 Frida-based mobile security toolkit mitmproxy 12.2.3 HTTPS interception proxy adb 1.0.41 Android Debug Bridge Python 3.14 Scripting and analysis pycryptodome 3.23.0 Cryptography library Key Techniques Learned 1. APK Static Analysis Pipeline APK → jadx (Java decompile) → read package structure → identify models → apktool (resources) → read strings.xml → find Firebase config → strings on .so files → identify libraries and obfuscation level 2. Firebase Remote Config Interrogation Any app using Firebase Remote Config can have its config fetched using credentials found in the APK:\ngoogle_api_key from strings.xml google_app_id from strings.xml Project ID from strings.xml The config endpoint is public (no server auth required):\nPOST https://firebaseremoteconfig.googleapis.com/v1/projects/{project_id}/namespaces/firebase:fetch?key={api_key} 3. Identifying Native-Level Obfuscation Signs that an app uses native-level protection:\nClasses with all-native methods (no Java implementation) System.loadLibrary() called in static initializer strings output shows Rust/C++ library names Custom TLS libraries (rustls, BoringSSL) in .so strings OkHttp class names not found during Frida class enumeration 4. Frida Gadget Injection (No Root Required) The technique:\nDecompile APK with apktool Copy frida-gadget-{ver}-android-{arch}.so into lib/{arch}/libfrida-gadget.so Inject System.loadLibrary(\u0026quot;frida-gadget\u0026quot;) into Application.onCreate() via smali edit Set android:extractNativeLibs=\u0026quot;true\u0026quot; and doNotCompress: - .so in apktool.yml Rebuild and resign with debug keystore Install: adb install --no-incremental app.apk Launch app (will freeze), forward port: adb forward tcp:27042 tcp:27042 Connect: frida -H 127.0.0.1:27042 -n Gadget -l script.js 5. Hooking Across the Java/Native Boundary When native code decrypts data and returns it to Java, hook the Java-side receiver:\n// Don\u0026#39;t try to hook native code — hook the Java method that calls it var nativeClass = Java.use(\u0026#39;g6.o\u0026#39;); nativeClass.c.implementation = function(input) { var result = this.c(input); // call original native method console.log(Java.use(\u0026#39;java.lang.String\u0026#39;).$new(result)); // intercept output return result; // return unmodified }; This works because the native method returns to Java — we intercept at the Java handoff, after decryption has occurred.\n6. smali Code Injection smali is the disassembled form of Dalvik bytecode. To inject code:\n# Load a string constant into register v0 const-string v0, \u0026#34;library-name\u0026#34; # Call System.loadLibrary(v0) invoke-static {v0}, Ljava/lang/System;-\u0026gt;loadLibrary(Ljava/lang/String;)V Always inject after the .locals N declaration and before any other code. The .locals count must be sufficient for your new registers.\n7. APK Signing When repackaging an APK, the original signature is lost. Android requires APKs to be signed. For testing purposes, a debug keystore is sufficient:\njava -jar uber-apk-signer.jar --apks app.apk --allowResign --overwrite # Signs with embedded debug keystore, creates v1+v2+v3 signatures Note: The debug keystore signature differs from the original, which means:\nPlay Store will reject the app (different signature) The app cannot receive updates through Play Store Some apps perform signature verification and will refuse to run 8. Network Topology Awareness When proxying mobile traffic:\nCheck which network the phone is on (adb shell ip route) Check Windows/Linux host IPs on all interfaces (ipconfig /all) Find the shared subnet between phone and proxy host Run proxy on the IP in the shared subnet Appendix: Code Artifacts SSL Unpinning Script (unpinning.js) Java.perform(function() { // TrustManagerImpl (Conscrypt) var TrustManagerImpl = Java.use(\u0026#39;com.android.org.conscrypt.TrustManagerImpl\u0026#39;); TrustManagerImpl.verifyChain.implementation = function( untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData ) { return untrustedChain; }; // NetworkSecurityTrustManager try { var NetworkSecurityTrustManager = Java.use( \u0026#39;android.security.net.config.NetworkSecurityTrustManager\u0026#39; ); var overloads = [ \u0026#39;[Ljava.security.cert.X509Certificate;,java.lang.String,java.lang.String\u0026#39;, \u0026#39;[Ljava.security.cert.X509Certificate;,java.lang.String,java.net.Socket\u0026#39;, \u0026#39;[Ljava.security.cert.X509Certificate;,java.lang.String,javax.net.ssl.SSLEngine\u0026#39; ]; overloads.forEach(function(sig) { var parts = sig.split(\u0026#39;,\u0026#39;); NetworkSecurityTrustManager.checkServerTrusted .overload.apply(null, parts) .implementation = function() { console.log(\u0026#39;[+] NST bypassed\u0026#39;); }; }); } catch(e) { console.log(\u0026#39;NST: \u0026#39; + e); } // TrustManagerImpl checkTrusted try { TrustManagerImpl.checkTrusted .overload(\u0026#39;[Ljava.security.cert.X509Certificate;\u0026#39;, \u0026#39;java.lang.String\u0026#39;, \u0026#39;javax.net.ssl.SSLSession\u0026#39;, \u0026#39;javax.net.ssl.SSLParameters\u0026#39;, \u0026#39;boolean\u0026#39;) .implementation = function() { return null; }; TrustManagerImpl.checkTrusted .overload(\u0026#39;[Ljava.security.cert.X509Certificate;\u0026#39;, \u0026#39;[B\u0026#39;, \u0026#39;[B\u0026#39;, \u0026#39;java.lang.String\u0026#39;, \u0026#39;java.lang.String\u0026#39;, \u0026#39;boolean\u0026#39;) .implementation = function() { return null; }; } catch(e) { console.log(\u0026#39;TMI: \u0026#39; + e); } console.log(\u0026#39;[+] SSL Unpinning complete\u0026#39;); }); Decryption Hook Script (decrypt-hook.js) Java.perform(function() { try { var g6o = Java.use(\u0026#39;g6.o\u0026#39;); g6o.c.implementation = function(input) { var result = this.c(input); try { var str = Java.use(\u0026#39;java.lang.String\u0026#39;).$new(result); if (str.length \u0026gt; 0) { console.log(\u0026#39;[DECRYPTED] \u0026#39; + str); } } catch(e) { console.log(\u0026#39;[DECRYPTED HEX] \u0026#39; + Array.from(result) .map(b =\u0026gt; (\u0026#39;0\u0026#39; + (b \u0026amp; 0xFF).toString(16)).slice(-2)) .join(\u0026#39;\u0026#39;)); } return result; }; console.log(\u0026#39;[+] g6.o.c hooked\u0026#39;); } catch(e) { console.log(\u0026#39;Hook error: \u0026#39; + e); } }); Complete Setup Script #!/bin/bash # Full setup for Mobiflix RE environment # Variables DEVICE_IP=\u0026#34;172.17.216.242\u0026#34; DEVICE_PORT=\u0026#34;39569\u0026#34; # Changes on reconnect, check adb devices PROXY_IP=\u0026#34;10.55.170.199\u0026#34; PROXY_PORT=\u0026#34;8080\u0026#34; APK=\u0026#34;mobiflix-patched-unsigned.apk\u0026#34; PACKAGE=\u0026#34;com.mobiflix.mobile\u0026#34; # Connect device adb connect ${DEVICE_IP}:${DEVICE_PORT} # Enable proxy (for mitmproxy capture) enable_proxy() { adb shell settings put global http_proxy ${PROXY_IP}:${PROXY_PORT} } # Disable proxy (for app to connect normally) disable_proxy() { adb shell settings put global http_proxy :0 adb shell settings delete global http_proxy } # Launch app and connect Frida launch_with_frida() { adb shell am force-stop ${PACKAGE} sleep 1 adb shell monkey -p ${PACKAGE} -c android.intent.category.LAUNCHER 1 sleep 3 adb forward tcp:27042 tcp:27042 frida -H 127.0.0.1:27042 -n Gadget -l ~/tools/unpinning.js } # Usage disable_proxy launch_with_frida This document covers the complete reverse engineering process of the Mobiflix Android application for educational purposes. The techniques described — static APK analysis, smali injection, Frida instrumentation, and certificate pinning bypass — are standard mobile security research methods applicable to security audits, bug bounty research, and understanding mobile application architecture.\n","permalink":"https://th4mjeed.pages.dev/posts/apk_reverse_engineering/","summary":"\u003chr\u003e\n\u003ch4 id=\"target-mobiflix-android-app-commobiflixmobile\"\u003eTarget: Mobiflix Android App (\u003ccode\u003ecom.mobiflix.mobile\u003c/code\u003e)\u003c/h4\u003e\n\u003cp\u003e\u003cstrong\u003eDate:\u003c/strong\u003e July 18, 2026\u003cbr\u003e\n\u003cstrong\u003eResearcher:\u003c/strong\u003e thamjeed\u003cbr\u003e\n\u003cstrong\u003eEnvironment:\u003c/strong\u003e Fedora WSL2 on Windows 11, Physical Android device (Xiaomi POCO, Android 13, arm64)\u003cbr\u003e\n\u003cstrong\u003eScope:\u003c/strong\u003e Educational — understanding mobile app architecture, API design, native library obfuscation, and dynamic instrumentation techniques.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cp\u003eThis document is a complete, step-by-step writeup of reverse engineering an Android streaming application from scratch. The goal was to understand:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eWhere the app fetches its content from\u003c/li\u003e\n\u003cli\u003eHow the app protects its API endpoints\u003c/li\u003e\n\u003cli\u003eWhat encryption scheme is used to hide configuration\u003c/li\u003e\n\u003cli\u003eHow certificate pinning is implemented and bypassed\u003c/li\u003e\n\u003cli\u003eThe full API surface area of the backend\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe process combined static analysis (decompiling the APK and reading Java/smali code) with dynamic analysis (Frida instrumentation, mitmproxy traffic interception).\u003c/p\u003e","title":"Android APK Reverse Engineering"},{"content":" Not my usual topic, but a fun project I wanted to share.\nCloudStream is an open-source Android app for streaming movies and TV shows. What makes it powerful is its plugin system - anyone can write a provider that scrapes or queries any source and plug it directly into the app. This post walks through exactly how I built Thamflix, a plugin that uses the TMDB API for metadata and Vidlink for streaming.\nWhat the Plugin Looks Like Here\u0026rsquo;s the Thamflix home screen running inside CloudStream:\nThamFlix home screen and movie details page. What is a CloudStream Plugin? A CloudStream plugin is a compiled Kotlin library packaged as a .cs3 file (which is just a renamed .zip containing a classes.dex and a manifest.json). The app loads it at runtime using a custom class loader. Plugins implement the MainAPI interface, which defines methods for the home page, search, loading a result page, and resolving video links.\nSetting Up the Development Environment The easiest way to get started is to fork the official TestPlugins template repository:\nhttps://github.com/recloudstream/TestPlugins This repo comes pre-configured with a GitHub Actions workflow that automatically builds your plugin and pushes the compiled .cs3 files to a builds branch whenever you push to master. This is the recommended approach — it avoids needing to set up Android SDK and Gradle locally.\nAfter forking:\nGo to Settings → Actions → General and set \u0026ldquo;Allow all actions and reusable workflows\u0026rdquo; Set \u0026ldquo;Read and write permissions\u0026rdquo; under the same menu Manually create a builds branch from master — the workflow checks this branch out on every run, so it must exist before the first build You also need to fix two things in the root build.gradle.kts before the build will succeed. The template ships with a -SNAPSHOT version of the CloudStream Gradle plugin that no longer resolves on JitPack, and an older Kotlin version that\u0026rsquo;s incompatible with the current CloudStream stubs:\n// Line 17 — fix the Gradle plugin version classpath(\u0026#34;com.github.recloudstream:gradle:81b1d424d2\u0026#34;) // Line 18 — fix Kotlin version to match CloudStream stubs classpath(\u0026#34;org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.0\u0026#34;) One more fix — the workflow\u0026rsquo;s clean step fails on the first run because there are no .cs3 files yet. Open .github/workflows/build.yml and add || true:\nrun: rm $GITHUB_WORKSPACE/builds/*.cs3 || true Project Structure Each plugin lives in its own folder at the repo root. The settings.gradle.kts automatically includes any directory that contains a build.gradle.kts, so there\u0026rsquo;s no manual registration needed:\nTestPlugins/ ├── Thamflix/ │ ├── build.gradle.kts │ └── src/main/kotlin/com/thamjeed/ThamflixProvider.kt ├── build.gradle.kts ├── settings.gradle.kts └── .github/workflows/build.yml Plugin build.gradle.kts This file declares the plugin\u0026rsquo;s metadata, which gets embedded into the generated plugins.json and displayed in CloudStream\u0026rsquo;s extension browser:\nversion = 1 cloudstream { description = \u0026#34;Thamflix — Movies and TV Shows via TMDB with Vidlink streaming\u0026#34; authors = listOf(\u0026#34;thamjeed\u0026#34;) status = 1 tvTypes = listOf(\u0026#34;Movie\u0026#34;, \u0026#34;TvSeries\u0026#34;) iconUrl = \u0026#34;https://www.google.com/s2/favicons?domain=www.themoviedb.org\u0026amp;sz=%size%\u0026#34; language = \u0026#34;en\u0026#34; } A few things worth noting:\nlanguage = \u0026quot;en\u0026quot; is required — CloudStream filters plugins by language and won\u0026rsquo;t display a plugin with a missing or mismatched language %size% in iconUrl is a template placeholder the app replaces with the appropriate icon size at runtime Do not add apiVersion here — it is not a valid property in the cloudstream block and will cause a build error Writing the Plugin The Plugin Class Every plugin needs a class annotated with @CloudstreamPlugin that extends BasePlugin. This is the entry point CloudStream uses to load the provider:\n@CloudstreamPlugin class ThamflixPlugin : BasePlugin() { override fun load() { registerMainAPI(ThamflixProvider()) } } The Provider Class The provider extends MainAPI and implements four core methods.\nHome Page The home page is defined using mainPageOf, which maps URL templates to display names. CloudStream calls getMainPage with a page number and the selected request, enabling infinite scroll:\noverride val mainPage = mainPageOf( \u0026#34;$tmdbBase/movie/popular?language=en-US\u0026amp;page=1\u0026#34; to \u0026#34;Popular Movies\u0026#34;, \u0026#34;$tmdbBase/tv/popular?language=en-US\u0026amp;page=1\u0026#34; to \u0026#34;Popular TV Shows\u0026#34;, // ... ) override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { val url = request.data.replace(\u0026#34;page=1\u0026#34;, \u0026#34;page=$page\u0026#34;) val isMovie = url.contains(\u0026#34;/movie/\u0026#34;) || url.contains(\u0026#34;discover/movie\u0026#34;) val response = app.get(url, headers = authHeaders).parsed\u0026lt;TmdbPageResponse\u0026gt;() val items = response.results.mapNotNull { it.toSearchResponse(isMovie) } return newHomePageResponse(request.name, items, hasNext = page \u0026lt; (response.total_pages ?: 1)) } Search Search hits both the TMDB movie and TV search endpoints and combines the results:\noverride suspend fun search(query: String): List\u0026lt;SearchResponse\u0026gt; { val movieResults = app.get( \u0026#34;$tmdbBase/search/movie?query=${query.encodeUrl()}\u0026amp;language=en-US\u0026amp;page=1\u0026#34;, headers = authHeaders ).parsed\u0026lt;TmdbPageResponse\u0026gt;().results.mapNotNull { it.toSearchResponse(isMovie = true) } val tvResults = app.get( \u0026#34;$tmdbBase/search/tv?query=${query.encodeUrl()}\u0026amp;language=en-US\u0026amp;page=1\u0026#34;, headers = authHeaders ).parsed\u0026lt;TmdbPageResponse\u0026gt;().results.mapNotNull { it.toSearchResponse(isMovie = false) } return movieResults + tvResults } Load (Result Page) The load function receives the URL/data string stored in the SearchResponse and returns a full LoadResponse with metadata and episode list. For TV shows, it fetches each season\u0026rsquo;s episode list from TMDB:\noverride suspend fun load(url: String): LoadResponse? { val data = parseJson\u0026lt;TmdbLoadData\u0026gt;(url) val detail = app.get( \u0026#34;$tmdbBase/${data.type}/${data.id}?language=en-US\u0026amp;append_to_response=credits,videos\u0026#34;, headers = authHeaders ).parsed\u0026lt;TmdbDetail\u0026gt;() // ... build and return MovieLoadResponse or TvSeriesLoadResponse } The key pattern here is using a serialized data class as the URL — instead of storing a raw URL, you serialize a TmdbLoadData object containing the TMDB ID and content type as a JSON string. This passes structured data cleanly between the search/home page and the load/loadLinks stages.\nLoad Links (Stream Resolution) This is where the actual video URL is resolved. Thamflix uses Vidlink, which requires a two-step process — encrypting the TMDB ID first, then fetching the stream playlist:\noverride suspend fun loadLinks( data: String, isCasting: Boolean, subtitleCallback: (SubtitleFile) -\u0026gt; Unit, callback: (ExtractorLink) -\u0026gt; Unit ): Boolean { val loadData = parseJson\u0026lt;TmdbLoadData\u0026gt;(data) // Step 1: Encrypt the TMDB ID val encRes = app.get( \u0026#34;https://enc-dec.app/api/enc-vidlink?text=${loadData.id}\u0026#34; ).parsed\u0026lt;EncryptResponse\u0026gt;() val encrypted = encRes.result ?: return false // Step 2: Fetch the HLS playlist val apiUrl = if (loadData.type == \u0026#34;movie\u0026#34;) { \u0026#34;https://vidlink.pro/api/b/movie/$encrypted\u0026#34; } else { \u0026#34;https://vidlink.pro/api/b/tv/$encrypted/${loadData.season}/${loadData.episode}\u0026#34; } val streamRes = app.get( apiUrl, headers = mapOf( \u0026#34;Referer\u0026#34; to \u0026#34;https://vidlink.pro/\u0026#34;, \u0026#34;Origin\u0026#34; to \u0026#34;https://vidlink.pro\u0026#34; ) ).parsed\u0026lt;VidlinkResponse\u0026gt;() val playlist = streamRes.stream?.playlist ?: return false callback( newExtractorLink( source = \u0026#34;Vidlink\u0026#34;, name = \u0026#34;Vidlink\u0026#34;, url = playlist, type = ExtractorLinkType.M3U8 ) { this.referer = \u0026#34;https://vidlink.pro/\u0026#34; this.quality = Qualities.Unknown.value } ) return true } Note that newExtractorLink uses a builder lambda — passing referer, quality, or isM3u8 as named parameters will cause a compilation error in the current CloudStream API.\nSetting Up the Repository CloudStream installs plugins from a repository defined by two JSON files.\nrepo.json This must be created manually in the builds branch — it is not auto-generated by the build system:\n{ \u0026#34;name\u0026#34;: \u0026#34;Thamflix\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;TMDB-powered Movies and TV Shows\u0026#34;, \u0026#34;manifestVersion\u0026#34;: 1, \u0026#34;pluginLists\u0026#34;: [ \u0026#34;https://raw.githubusercontent.com/th4mjeed/TestPlugins/builds/plugins.json\u0026#34; ] } plugins.json This is auto-generated by the GitHub Actions workflow on every push. It contains metadata for every plugin including the .cs3 URL, file hash, and size. CloudStream uses the hash to verify the downloaded file.\nOne thing to watch out for: newer versions of the build system add extra fields (jarUrl, jarFileSize, jarHash) to plugins.json that older versions of CloudStream don\u0026rsquo;t expect. If your plugin shows up in the repo list but not in the extensions list, manually edit plugins.json in the builds branch and remove those extra fields.\nInstalling in CloudStream Add this URL in CloudStream under Extensions → Add Repository:\nhttps://raw.githubusercontent.com/th4mjeed/TestPlugins/builds/repo.json If a plugin doesn\u0026rsquo;t appear after adding the repo:\nVerify language in your build.gradle.kts matches your CloudStream language setting Check the content type filter in app settings — if TvSeries is disabled, any plugin with TvSeries in its tvTypes will be hidden Remove the repo, force close the app, clear app cache from Android settings, and re-add Source Code The full source is available at GitHub.\nReference Here\u0026rsquo;s what I referenced: Cloudstream Plugin Development Guide.\n","permalink":"https://th4mjeed.pages.dev/posts/cloudstream-plugin/","summary":"\u003cblockquote\u003e\n\u003cp\u003eNot my usual topic, but a fun project I wanted to share.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003cp\u003eCloudStream is an open-source Android app for streaming movies and TV shows. What makes it powerful is its plugin system - anyone can write a provider that scrapes or queries any source and plug it directly into the app. This post walks through exactly how I built \u003cstrong\u003eThamflix\u003c/strong\u003e, a plugin that uses the TMDB API for metadata and Vidlink for streaming.\u003c/p\u003e","title":"Building a CloudStream Plugin from Scratch - How I Made Thamflix"},{"content":"Introduction I wanted to understand how LSF actually works, not just at a surface “submit job, job runs” level, but the real mechanics. Most places that use LSF don’t document it well, and when they do, it’s written assuming you already run a production cluster.\nSo I decided to build a small LSF Community Edition cluster at home and treat it like an EDA-style environment: master node, execution nodes, login-style submission, interactive jobs, X11, the whole thing.\nThis post is not a clean guide. It’s more like notes from what actually happened.\nChapter 1: Why LSF and Not Slurm I already have experience with Slurm, and I even wrote about using Slurm as a compilation farm. Slurm is straightforward. LSF is not.\nBut that’s exactly why I wanted to learn it.\nLSF still shows up in EDA, semiconductor companies, and older HPC environments. And the way it thinks about hosts, submission, and execution is very different from Slurm.\nIn Slurm, a login node is mostly just a machine that can talk to slurmctld.\nIn LSF, every host must be known, typed, and classified. Even submit-only machines.\nThat difference alone is worth learning.\nChapter 2: The Cluster Layout I Used I kept the setup intentionally small.\nlsf-master\nRuns LIM, mbatchd, acts as master candidate.\nlsf-node1\nExecution node.\nfedora\nMy Fedora workstation, acting as a login / submission host.\nAll machines could resolve each other via /etc/hosts. No DNS magic.\nI used Rocky Linux for lsf-master and lsf-node1, and Fedora for my submission machine.\nChapter 3: Getting LSF This part deserves its own chapter because this is where things already got weird.\nThere is no obvious “download LSF” button.\nI actually found the IBM Spectrum LSF Community Edition through a Reddit post. That post linked to IBM’s site, which then required me to:\nCreate an IBM account Log in Accept licensing terms Navigate through IBM’s download portal Only after all that was I able to download the tarball.\nWhat I ended up with was something like:\nlsfsce10.2.0.15-x86_64.tar.Z This already tells you something important:\nIBM ships prebuilt binaries They are tied to a specific glibc and kernel baseline That becomes important later when Fedora starts complaining.\nChapter 3.1: Extracting the Tarball On the master node (lsf-master), I extracted it under /tmp first:\ntar -xvf lsfsce10.2.0.15-x86_64.tar.Z cd lsfsce10.2.0.15-x86_64 Inside, you don’t get a fancy installer. You get scripts.\nThe real entry point is:\n./lsfinstall But do not run it blindly.\nChapter 3.2: install.config Is the Real Installer LSF installation is driven almost entirely by a file called:\ninstall.config If you mess this up, the installer will still run, but you’ll regret it later.\nThis is roughly what I used (simplified to the important parts):\nLSF_TOP=\u0026#34;/usr/local/lsf\u0026#34; LSF_ADMINS=\u0026#34;edauser\u0026#34; LSF_CLUSTER_NAME=\u0026#34;lsfcluster\u0026#34; LSF_MASTER_LIST=\u0026#34;lsf-master\u0026#34; LSF_SERVER_HOSTS=\u0026#34;lsf-master lsf-node1\u0026#34; ENABLE_EGO=\u0026#34;Y\u0026#34; EGO_DAEMON_CONTROL=\u0026#34;Y\u0026#34; LSF_LOCAL_RESOURCES=\u0026#34;[resource mg]\u0026#34; Things that matter here:\nLSF_TOP\nThis decides everything. Binaries, configs, logs. I kept it simple.\nLSF_ADMINS\nThis user must exist. I created edauser beforehand.\nLSF_CLUSTER_NAME\nThis name shows up everywhere later. Pick once, don’t change it.\nLSF_MASTER_LIST\nThis is not optional. If this is wrong, LIM will never behave.\nLSF_SERVER_HOSTS\nThese are execution-capable hosts. Submission-only hosts do NOT go here.\nAt this stage, Fedora was not included anywhere.\nChapter 3.3: Running the Installer Only after editing install.config did I run:\n./lsfinstall -f install.config The installer:\ncreates /usr/local/lsf drops binaries under versioned directories writes configs under /usr/local/lsf/conf When it finished, I had:\n/usr/local/lsf/10.1/ /usr/local/lsf/conf/ /usr/local/lsf/work/ At this point, LSF was technically “installed”.\nThat does not mean usable.\nChapter 3.4: Environment Setup LSF does nothing unless you source its environment and start the daemons.\nOn the master and nodes:\nsource /usr/local/lsf/conf/profile.lsf Without this:\nbhosts won’t work lsid won’t work errors will be extremely misleading I added this to the admin user’s shell profile later, but initially I sourced it manually.\nTo start the daemons I used the commad:\nlsf_daemons start Chapter 4: The First Big Reality Check — Hosts Must Be Known One mistake I made early was assuming that copying the tarball to another machine and sourcing profile.lsf was enough.\nIt is not.\nLSF does not work like that.\nWhen I ran:\nbhosts on my Fedora machine, I kept getting:\nFailed in an LSF library call: LIM is down; try later Even though:\nI could ping the master LIM ports were reachable SSH worked The problem wasn’t networking.\nThe problem was that LSF didn’t know my Fedora machine existed.\nChapter 5: Teaching LSF That Fedora Exists (and What It Is) This was the first major “oh” moment.\nI kept assuming that since Fedora was only a submit host, LSF wouldn’t really care about it. That assumption was wrong.\nIf a machine is going to submit jobs, it must appear in the cluster definition file:\n/usr/local/lsf/conf/lsf.cluster.\u0026lt;clustername\u0026gt; Even if:\nit never runs jobs it never runs RES it is just a login box LSF is very literal about this.\nMy Host section eventually looked like this:\nBegin Host HOSTNAME model type server RESOURCES lsf-master ! ! 1 (mg) lsf-node1 ! ! 1 () fedora ! ! 0 () End Host That server 0 is important. Fedora is not an execution host. It only submits jobs.\nOnce I added Fedora here and ran:\nlsadmin reconfig the errors changed. They didn’t disappear, but they changed, which is usually how you know you’re moving forward with LSF.\nAt this point, I thought I was done.\nI wasn’t.\nEven after Fedora was listed in lsf.cluster, I kept getting this error:\nCannot find restarted or newly submitted job\u0026#39;s submission host and host type This one took time to understand.\nLSF doesn’t just want to know that a host exists.\nIt also wants to know what kind of host it is:\narchitecture operating system That information does not live in lsf.cluster.\nIt lives in:\n/usr/local/lsf/conf/lsf.shared This is the file almost everyone forgets.\nI had to explicitly define:\nthe host model (X86_64) the host type (LINUX) and map every host, including Fedora This is what finally fixed it:\nBegin Host HOSTNAME model type lsf-master X86_64 LINUX lsf-node1 X86_64 LINUX fedora X86_64 LINUX End Host After saving this file, I ran:\nlsadmin reconfig Only after this did bhosts stop rejecting Fedora and job submission start behaving like it should.\nThis was a big lesson for me:\nLSF will happily run with half the information missing, but it will fail in ways that make it feel like something much deeper is broken.\nChapter 6: Interactive Jobs Work… Until X11 Shows Up Once submission worked, I tried:\nbsub -Is -XF xterm And hit a whole new class of errors:\nX11 connection rejected because of wrong authentication At this point, LSF was fine.\nThe cluster was fine.\nThis was pure SSH + X11 pain.\nWhat Was Actually Missing\nThe main issues were:\nxauth not installed on all nodes X11 forwarding not consistently enabled SSH key-based auth not fully set up Installing xauth everywhere and making sure:\nX11Forwarding yes X11UseLocalhost no was set on all machines finally fixed it.\nOnly after this did:\nbsub -Is -XF -m lsf-master xterm actually open a window.\nClosing Thoughts At this point, the cluster was stable enough for what I wanted to test.\nI initially planned to go one step further and add a proper license server using FlexLM, similar to how it’s done in real EDA environments. The idea was to tie license availability into scheduling and see how LSF behaves under those constraints.\nBut I decided to stop here.\nThe goal of this setup was to understand:\nhow LSF components talk to each other how submit hosts differ from execution hosts and what minimum configuration is actually required for things to work That part was done.\nLicense servers can come later as a separate iteration.\n","permalink":"https://th4mjeed.pages.dev/posts/setting-up-community-edition-lsf/","summary":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eI wanted to understand how LSF actually works, not just at a surface “submit job, job runs” level, but the \u003cem\u003ereal\u003c/em\u003e mechanics. Most places that use LSF don’t document it well, and when they do, it’s written assuming you already run a production cluster.\u003c/p\u003e\n\u003cp\u003eSo I decided to build a small LSF Community Edition cluster at home and treat it like an EDA-style environment: master node, execution nodes, login-style submission, interactive jobs, X11, the whole thing.\u003c/p\u003e","title":"Setting up the Community Edition of IBM LSF (Load Sharing Facility)"},{"content":"In this guide, we walk through the process of setting up a Samba Active Directory Domain Controller. The goal is to create a fully functional AD environment with the ability to join Windows and Linux clients to the domain.\nThis setup mirrors how Microsoft Active Directory works, but fully powered by open‑source software.\nWe\u0026rsquo;ll be using Fedora Linux to configure Samba AD, but you can use any Linux\nPreparing the Domain Controller System We begin by configuring Fedora 43 to act as a domain controller. The hostname must reflect the fully qualified domain name of the AD server.\nSet the hostname:\nhostnamectl set-hostname dc1.internal.lan Add the server address to /etc/hosts so DNS lookups resolve locally during installation:\n10.10.40.90 dc1.internal.lan dc1 Next, update packages and install the Samba DC components along with Kerberos and DNS utilities:\nsudo dnf update -y sudo dnf install -y samba samba-dc samba-dns samba-winbind krb5-workstation bind-utils Fedora uses systemd‑resolved by default, which conflicts with Samba’s internal DNS. We disable it and ensure Samba will handle DNS:\nsudo systemctl disable --now systemd-resolved sudo rm -f /etc/resolv.conf Then create a new resolv.conf pointing DNS to the AD server itself:\necho \u0026#34;nameserver 127.0.0.1\u0026#34; | sudo tee /etc/resolv.conf echo \u0026#34;search internal.lan\u0026#34; | sudo tee -a /etc/resolv.conf At this point, the server is ready for domain provisioning.\nProvisioning the Active Directory Domain Samba includes a provisioning tool that creates the directory structure, Kerberos configuration, and internal DNS zones.\nRun provisioning interactively:\nsudo samba-tool domain provision --use-rfc2307 --interactive During setup:\nRealm can be set to INTERNAL.LAN Domain name can be INTERNAL Server role must be dc (domain controller) Select SAMBA_INTERNAL DNS backend When provisioning completes, start the Samba service:\nsudo systemctl enable --now samba To verify that DNS is functioning, check SRV records for Kerberos and LDAP:\nhost -t SRV _kerberos._udp.internal.lan host -t SRV _ldap._tcp.internal.lan host -t A dc1.internal.lan Confirm that Samba’s internal services are running properly:\nsudo samba-tool processes Verifying Kerberos Authentication Kerberos is central to Active Directory authentication. We test it using the built‑in Administrator account.\nkinit administrator@INTERNAL.LAN klist If everything is configured correctly, a Kerberos ticket will be displayed.\nCreating Users in Active Directory To add a test user:\nsamba-tool user create employee1 This user can later be used to log in to domain‑joined machines.\nJoining a Fedora Client to the Domain On the Fedora client, install the tools required for AD domain membership:\nsudo dnf install -y realmd sssd adcli oddjob oddjob-mkhomedir samba-winbind-clients Discover the domain:\nrealm discover internal.lan Join the domain using administrative credentials:\nsudo realm join internal.lan -U administrator Enable automatic home directory creation for domain users:\nsudo pam-auth-update --enable mkhomedir Now, domain accounts can be used to log in through the graphical login screen using:\nINTERNAL\\employee1 Joining a Windows 11 Client to the Domain Windows 11 systems can be joined to the Samba Active Directory domain, enabling centralized authentication just like in a Microsoft AD environment.\nConfigure DNS on Windows 11 Before joining the domain, ensure the Windows machine uses the domain controller for DNS resolution:\nOpen Settings → Network \u0026amp; Internet\nSelect the active network adapter\nChoose Edit DNS settings\nSet it to Manual and configure:\nPreferred DNS: 10.10.40.90 Test DNS resolution using Command Prompt:\nping dc1.internal.lan Join Windows to the Domain Open Run (Win + R) → type: sysdm.cpl Go to the Computer Name tab Click Change and select Domain Enter: internal.lan Provide domain credentials when prompted (such as INTERNAL\\administrator) Reboot the system Validate Domain Login After reboot, log in using:\nINTERNAL\\employee1 Then verify domain membership:\nsysteminfo | findstr /i domain Final Notes With Samba successfully serving as an Active Directory Domain Controller, Linux systems can now authenticate against the domain and Kerberos security is fully operational.\nFuture enhancements may include:\nInstalling RSAT on Windows 11 for easy management of User and Groups Configuring Group Policy through Samba tools Securing DNS and directory traffic with TLS certificates Adding domain file services with Windows‑compatible permissions Integrating an AD Certificate Services alternative for Kerberos PKINIT ","permalink":"https://th4mjeed.pages.dev/posts/setting-up-samba-ad/","summary":"\u003cp\u003eIn this guide, we walk through the process of setting up a Samba Active Directory Domain Controller. The goal is to create a fully functional AD environment with the ability to join Windows and Linux clients to the domain.\u003c/p\u003e\n\u003cp\u003eThis setup mirrors how Microsoft Active Directory works, but fully powered by open‑source software.\u003c/p\u003e\n\u003cp\u003eWe\u0026rsquo;ll be using Fedora Linux to configure Samba AD, but you can use any Linux\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"preparing-the-domain-controller-system\"\u003ePreparing the Domain Controller System\u003c/h2\u003e\n\u003cp\u003eWe begin by configuring Fedora 43 to act as a domain controller. The hostname must reflect the fully qualified domain name of the AD server.\u003c/p\u003e","title":"Setting Up Samba as an Active Directory Domain Controller on Linux"},{"content":" Note: 📢 This document is based on my understanding of SLURM and is in no way a detailed guide covering every single topic. Take this as a practical guide from a noob’s perspective diving into it.\nIntroduction: This guide is designed to help you effectively use the SLURM scheduler on Rocky Linux 9.5 server. The Server allows you to run computational jobs using both interactive and non-interactive modes. The goal here is to make a compilation farm, although this guide specifically focuses on compiling the Linux kernel, one should note that this may also be used to compile any other tool given that the prerequisites and dependencies are known.\nLab Infrastructure: The following are all on VMware ESXI\nMaster:\nCPUs 4 Memory 4 GB Hard disk 20 GB Hostname: master Node 1:\nCPUs 4 Memory 4 GB Hard disk 40 GB Hostname: node1 Node 2:\nCPUs 8 Memory 8 GB Hard disk 40 GB Hostname: node2 Network File Storage\nSince compiling creates dozens of files, at least 30 GB is required for a successful compilation. Used the existing testing server assigned to me. NFS share path located in /mnt/slrum_share Every instance has Rocky Linux 9.5 installed with SSH, root login and defined IP of all 4 nodes in the /etc/hosts file.\nThe architecture diagram looks like this:\nChapter 1: The installation: Install and configure dependencies\nInstallation of slurm requires EPEL repo to be installed across all instances, install and enable it via:\ndnf config-manager --set-enabled crb dnf install epel-release sudo dnf groupinstall \u0026#34;Development Tools\u0026#34; sudo dnf install munge munge-devel rpm-build rpmdevtools python3 gcc make openssl-devel pam-devel MUNGE is an authentication mechanism for secure communication between Slurm components. Configure it on all instances using:\nsudo useradd munge sudo mkdir -p /etc/munge /var/log/munge /var/run/munge sudo chown munge:munge /usr/local/var/run/munge sudo chmod 0755 /usr/local/var/run/munge On Master:\nsudo /usr/sbin/create-munge-key sudo chown munge:munge /etc/munge/munge.key sudo chmod 0400 /etc/munge/munge.key Copy the key to both nodes:\nscp /etc/munge/munge.key root@node1:/etc/munge/ scp /etc/munge/munge.key root@node2:/etc/munge/ Start and enable the service:\nsudo systemctl enable --now munge Installation of SLURM\nSlurm is available in the EPEL repo. Install on all 3 instances:\nsudo dnf install slurm slurm-slurmd slurm-slurmctld slurm-perlapi If by any chance packages are not available, download tar file from SchedMD Downloads, extract, compile, and install using:\nmake -j$(nproc) sudo make install Chapter 2: The Configuration: Slurm configuration\nOn all 3 instances:\nsudo useradd slurm sudo mkdir -p /etc/slurm /var/spool/slurmctld /var/spool/slurmd /var/log/slurm sudo chown slurm:slurm /var/spool/slurmctld /var/spool/slurmd /var/log/slurm Edit the configuration on master:\nsudo nano /etc/slurm/slurm.conf Ensure the following key lines are present and correctly configured:\nClusterName=debug SlurmUser=slurm ControlMachine=slurm-master SlurmctldPort=6817 SlurmdPort=6818 AuthType=auth/munge StateSaveLocation=/var/spool/slurmctld SlurmdSpoolDir=/var/spool/slurmd SwitchType=switch/none MpiDefault=none SlurmctldPidFile=/var/run/slurmctld.pid SlurmdPidFile=/var/run/slurmd.pid ProctrackType=proctrack/pgid ReturnToService=1 SchedulerType=sched/backfill SlurmctldTimeout=300 SlurmdTimeout=30 NodeName=node1 CPUs=4 RealMemory=3657 State=UNKNOWN NodeName=node2 CPUs=8 RealMemory=7682 State=UNKNOWN PartitionName=debug Nodes=node[1-2] Default=YES MaxTime=INFINITE State=UP Copy configuration to nodes:\nscp /etc/slurm/slurm.conf root@node1:/etc/slurm/slurm.conf scp /etc/slurm/slurm.conf root@node2:/etc/slurm/slurm.conf Start and enable services:\nsudo systemctl enable --now slurmctld sudo systemctl enable --now slurmd Firewall Configuration:\nOpen required ports:\nsudo firewall-cmd --permanent --add-port=6817/tcp sudo firewall-cmd --permanent --add-port=6818/tcp sudo firewall-cmd --permanent --add-port=6819/tcp sudo firewall-cmd --reload Chapter 3: Testing and Introduction to the commands: (While this is a short guide on the commands and its flags, you could always use man pages to understand it more deeply)\nsinfo:\nDisplays node and partition information:\nsinfo srun:\nRuns commands interactively on compute nodes:\nsrun -N2 -n2 nproc sbatch:\nSubmits a job script:\nsbatch testjob.sh squeue:\nDisplays details of currently running jobs:\nsqueue scancel:\nCancels a submitted job:\nscancel 1 scontrol:\nDisplays detailed job and node information:\nscontrol show job 1 scontrol show partition Chapter 4: Setting up the NFS storage. It is a good idea to have shared storage for SLURM. Install nfs-utils:\nsudo dnf install nfs-utils On the NFS server:\nmkdir /srv/slurm_share nano /etc/exports Add the following line:\n/srv/slurm_share 10.10.40.0/24(rw,sync,no_subtree_check,no_root_squash) Open necessary ports:\nfirewall-cmd --permanent --add-service=rpc-bind firewall-cmd --permanent --add-port={5555/tcp,5555/udp,6666/tcp,6666/udp} firewall-cmd --reload Export and enable the service:\nexportfs -v systemctl enable --now nfs-server On the master and compute nodes:\nsudo mkdir /mnt/slurm_share Add the mount in /etc/fstab:\n10.10.40.0:/srv/slurm_share /mnt/slurm_share nfs defaults 0 0 Reboot machines and verify the share mounts properly.\nChapter 5: Setting up the Compile/Build Environment Install kernel build dependencies:\nsrun -n2 -N2 sudo dnf groupinstall \u0026#34;Development Tools\u0026#34; -y \u0026amp;\u0026amp; sudo dnf install ncurses-devel bison flex elfutils-libelf-devel openssl-devel wget bc dwarves -y Download the Linux kernel source from kernel.org:\nwget [https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.14.8.tar.xz](https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.14.8.tar.xz) tar xvf linux-6.14.8.tar.xz Define architecture-specific config:\nmake defconfig Create compile_kernel.sh on the shared directory:\n#!/bin/bash #SBATCH --job-name=kernel_build #SBATCH --output=kernel_build_%j.out #SBATCH --error=kernel_build_%j.err #SBATCH --time=03:00:00 #SBATCH --nodes=1 #SBATCH --cpus-per-task=8 #SBATCH --mem=8G KERNEL_SOURCE_PATH=\u0026#34;/mnt/slurm_share/linux-6.8.9\u0026#34; BUILD_OUTPUT_DIR=\u0026#34;/mnt/slurm_share/kernel_builds/${SLURM_JOB_ID}\u0026#34; mkdir -p \u0026#34;$BUILD_OUTPUT_DIR\u0026#34; cd \u0026#34;$KERNEL_SOURCE_PATH\u0026#34; NUM_MAKE_JOBS=${SLURM_CPUS_PER_TASK} make -j\u0026#34;${NUM_MAKE_JOBS}\u0026#34; ARCH=x86_64 Image modules dtbs if [ $? -eq 0 ]; then cp \u0026#34;$KERNEL_SOURCE_PATH/arch/x86/boot/bzImage\u0026#34; \u0026#34;$BUILD_OUTPUT_DIR/\u0026#34; else echo \u0026#34;Kernel compilation failed.\u0026#34; fi ","permalink":"https://th4mjeed.pages.dev/posts/slurm-as-a-compilation-farm/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eNote:\u003c/strong\u003e 📢 This document is based on my understanding of SLURM and is in no way a detailed guide covering every single topic. Take this as a practical guide from a noob’s perspective diving into it.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"introduction\"\u003e\u003cstrong\u003eIntroduction:\u003c/strong\u003e\u003c/h2\u003e\n\u003cp\u003eThis guide is designed to help you effectively use the SLURM scheduler on Rocky Linux 9.5 server. The Server allows you to run computational jobs using both interactive and non-interactive modes. The goal here is to make a compilation farm, although this guide specifically focuses on compiling the Linux kernel, one should note that this may also be used to compile any other tool given that the prerequisites and dependencies are known.\u003c/p\u003e","title":"SLURM as a Compilation Farm"}]