april 24, 2026 // 6 min read
hey!! :33 saw this on tiktok recently, and my friend kept hyping up this music streaming service called Lane, so i decided to poke around and found some funny stuff...
so what even is Lane
according to the devs, Lane is yet another streaming app that supposedly mirrors tracks from Spotify and SoundCloud (lethal combination /joke)
sending the app theme on every request

why? unclear. maybe trying to track who uses light vs dark theme (/joke /sarc)
lyrics sync
or "what if we sync track lyrics with json..."
when you open any track, the app hits https://API_URL/track/trackid/lyrics (API_URL is region-specific - theres ruapi and just api. the only difference is ruapi points to a server on Yandex Cloud, while api is behind Cloudflare)

where are the tracks (and other static) stored?
album art is pulled from i.scdn.co:

and tracks come from their bucket somewhere on Cloudflare R2/S3:

/events - is this telemetry?
two very interesting headers get sent here:
X-Connection-Quality- sourced viaNetworkQualityUtilsX-Is-Vpn- vpn flag. question: why?..

also there are some interesting event types:
NewBadgeEvent- badge award. some profile flair, described in their telegram channelNewNotificationEvent- some notification display, not that interestingPremiumActivatedEvent- update premium statusProxyRequestEvent- the fuck is dat????
after each /events response, the server sends back a new jwt, the client saves it and immediately reconnects
ProxyRequestEvent
the server can send an arbitrary HTTP request (url, method, headers, body), and
the client will execute it on its own behalf and return the result back via
POST /p2p-callback. this is literally a p2p proxy where the server uses users
as relay nodes the client resolves via Cloudflare DoH (1.1.1.1/dns-query) with a
separate OkHttpClient, bypassing the system DNS/corporate network (lol)
practically speaking this means:
- traffic to third-party resources comes from the users IP, not Lanes servers
- potentially bypassing rate-limits and geo-blocks using the user base (mb?)
- if the user is on a corporate network - possibility to reach internal addresses (though given the target audience - i'm not sure...)
roughly how it works: server sends a GET /events, delivers
ProxyRequestEvent{url, method, headers, body} in it, client makes this request
itself (OkHttpClient), POST /p2p-callback ->
P2PCallbackRequest{requestId, responseBody, statusCode}.
so... the client is a transparent HTTP proxy....??? the server can ask it to execute a request to any url, and the client will return the response.
native
theres also a libbnit.so something like AWS Signature V4 but homegrown. every
request gets signed: method + path + query + body are taken, a time offset is
added, all of this is signed with a hardcoded key inside the native lib and sent
as X-Core-Token. the server verifies the signature and if it doesn't match - you
get rejected
premium
theoretically its entirely client-side: in AudioQualityViewModel, the server receives streamQuality=HIGH as a plain string with no server-side permission check (at least no obvious one on the client side)
update (2026-07-20)
decided to pull the apk again and diff the decompile against what i wrote above. a bunch of stuff got quietly reworked, some of it actually addresses things i complained about, some of it is worse
ProxyRequestEvent got a "if network sucks, don't" check
ClientEventService now calls NetworkQualityUtils.a(context) before running a
proxy request - if you're not on wifi/ethernet, or on cellular below 5mbps, it
skips execution entirely and just replies to /p2p-callback with
(requestId, 0, "Slow connection"). so the p2p proxy thing is still fully
there, they just added throttling so it doesnt eat someone's mobile data. still
no url allowlist - server can still ask the client to hit anything
dns resolution is now a 3-way race
previously (well, previously = what i saw before) it was just cloudflare doh.
now NetworkModule builds three DnsOverHttps clients - cloudflare
(1.1.1.1/dns-query), google (8.8.8.8/dns-query), and yandex
(common.dot.dns.yandex.net/dns-query) - and fires all of them plus the system
resolver in parallel via ExecutorCompletionService, takes whichever answers
first, cancels the rest. yandex dns joining the mix pretty strongly implies this
is a "what if cloudflare doh is blocked in Russia" contingency
also there's a DPIRetryInterceptor now
does what it says - retries a failed request up to 3 times with increasing
backoff (i * 1000ms), swallows everything except UnknownHostException /
ConnectException. the name alone is a pretty strong hint about what kind of
network environment they're expecting their users to be in
the api host can now be swapped out entirely, server-side
BaseServerSetting used to be effectively static. now it's mutable, and
ServerDiscoveryRepository decides on startup: if your timezone smells russian,
hardcode ruapi.sklane.com and bail. otherwise, hit some discovery endpoint,
get back ApiReserveResponse{apiUrl, cdnUrl}, and repoint the entire app's
api/cdn base url to whatever the server said, retrying once through a bare
client if the first attempt 401s. HostSelectionInterceptor then force-rewrites
the host of any outgoing request to match, unless it's already going to
laneaudiobucket or sndcdn.com (so yeah, thats the real bucket name)
libbnit.so verifyLicense is a decoy
opened this up in ida this time. there's a second native method now,
verifyLicense(String token), registered right next to cdrl in JNI_OnLoad.
cracked it open expecting some RSA verification given the name - nope. it
completely ignores the token argument, xor-decodes a hardcoded string
("LICENSE_VALID_RSA_4096") out of .rodata, and returns that via
NewStringUTF. that's the whole function. no license is being verified, ever,
regardless of what you pass in. either dead code from something that was never
finished, or a decoy for anyone poking at the Java side that decompiles cleanly
and looks like it does something
cdrl (the signing function) pulls in boot_id + /proc/self/maps
this is the one that actually surprised me, and i had to go back and fix my own
read of it after actually stepping through the disassembly instead of eyeballing
strings. the signing routine isn't just "hash some fields with a hardcoded key"
anymore (if it ever was) - both paths are xor-obfuscated in .rodata the same
way the class name in JNI_OnLoad is, decoded on the fly with the same
(i % 13) + 74 xor key, then opened as ifstream, one line read, closed:
/proc/sys/kernel/random/boot_id- a per-boot unique id, survives app restarts but changes on reboot/proc/self/maps- but only the first line gets read, not the whole file. so this isn't scanning the full module list for frida/xposed/whatever like i first assumed - it's just grabbing whatever's mapped first (typically the base address range of the executable itself) as one more piece of entropy
on top of that theres a /dev/urandom read (gettimeofday fallback if that
fails) hex-encoded into a 32-char nonce, and all of it gets fed into what looks
like a hash/hmac step (sub_ACF0) alongside method/path/query/body/time-offset.
so its less "anti-frida check baked into signing" and more "boot-scoped device
fingerprint + nonce mixed into the homegrown sigv4" - still moderately clever
for something this vibecoded, just not the dramatic tamper-detection i made it
sound like initially
conclusions???
we know this app can, on server command, go pretty much anywhere (no allowlist,
though there probably should be), the token-related stuff is pushed into native
code + i have a strong suspicion this is all vibecoded, especially after
x-theme: light and such a weird lyrics sync implementation when you could've
just used elrc