Tag sockets via OkHttp EventListener to cover proxied routes

The SocketFactory-based tagging missed SOCKS proxy (Tor) connections:
ConnectPlan.connectSocket builds those sockets with Socket(proxy)
directly, bypassing the configured SocketFactory, so the raw fd was
created untagged on the TaskRunner thread and tripped StrictMode's
UntaggedSocketViolation. The TaggingInterceptor didn't help either — it
runs on the call/dispatch thread, not the connect thread.

Replace TaggedSocketFactory, TaggedDns, and TaggingInterceptor with a
single TrafficStatsEventListener that sets the thread stats tag in
dnsStart/connectStart. Those callbacks fire on the exact thread that then
opens the socket, for every route type (direct, HTTP, and SOCKS), so one
listener covers them all. Wire it into the shared root client and the
WebDav client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
greenart7c3
2026-06-08 11:13:00 -03:00
co-authored by Claude Opus 4.8
parent 1a40be3efa
commit 1318f0ae60
6 changed files with 34 additions and 89 deletions
@@ -25,8 +25,6 @@ import com.greenart7c3.nostrsigner.Amber
import java.net.InetSocketAddress
import java.net.Proxy
import java.time.Duration
import javax.net.SocketFactory
import okhttp3.Dns
import okhttp3.OkHttpClient
object HttpClientManager {
@@ -35,9 +33,7 @@ object HttpClientManager {
.Builder()
.followRedirects(true)
.followSslRedirects(true)
.socketFactory(TaggedSocketFactory(SocketFactory.getDefault()))
.dns(TaggedDns(Dns.SYSTEM))
.addInterceptor(TaggingInterceptor())
.eventListener(TrafficStatsEventListener())
.build()
}
@@ -1,14 +0,0 @@
package com.greenart7c3.nostrsigner.okhttp
import android.net.TrafficStats
import java.net.InetAddress
import okhttp3.Dns
class TaggedDns(private val delegate: Dns) : Dns {
override fun lookup(hostname: String): List<InetAddress> {
if (TrafficStats.getThreadStatsTag() == -1) {
TrafficStats.setThreadStatsTag(0x0001)
}
return delegate.lookup(hostname)
}
}
@@ -1,53 +0,0 @@
package com.greenart7c3.nostrsigner.okhttp
import android.net.TrafficStats
import java.net.InetAddress
import java.net.Socket
import javax.net.SocketFactory
class TaggedSocketFactory(private val delegate: SocketFactory) : SocketFactory() {
private fun tag() {
if (TrafficStats.getThreadStatsTag() == -1) {
TrafficStats.setThreadStatsTag(0x0001)
}
}
override fun createSocket(): Socket {
tag()
return delegate.createSocket()
}
override fun createSocket(host: String?, port: Int): Socket {
tag()
val socket = delegate.createSocket(host, port)
tagSocket(socket)
return socket
}
override fun createSocket(host: String?, port: Int, localHost: InetAddress?, localPort: Int): Socket {
tag()
val socket = delegate.createSocket(host, port, localHost, localPort)
tagSocket(socket)
return socket
}
override fun createSocket(host: InetAddress?, port: Int): Socket {
tag()
val socket = delegate.createSocket(host, port)
tagSocket(socket)
return socket
}
override fun createSocket(address: InetAddress?, port: Int, localAddress: InetAddress?, localPort: Int): Socket {
tag()
val socket = delegate.createSocket(address, port, localAddress, localPort)
tagSocket(socket)
return socket
}
private fun tagSocket(socket: Socket) {
if (socket.isConnected || socket.isBound) {
TrafficStats.tagSocket(socket)
}
}
}
@@ -1,14 +0,0 @@
package com.greenart7c3.nostrsigner.okhttp
import android.net.TrafficStats
import okhttp3.Interceptor
import okhttp3.Response
class TaggingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
if (TrafficStats.getThreadStatsTag() == -1) {
TrafficStats.setThreadStatsTag(0x0001)
}
return chain.proceed(chain.request())
}
}
@@ -0,0 +1,31 @@
package com.greenart7c3.nostrsigner.okhttp
import android.net.TrafficStats
import java.net.InetSocketAddress
import java.net.Proxy
import okhttp3.Call
import okhttp3.EventListener
/**
* Tags the calling thread with a TrafficStats stats tag so the sockets OkHttp
* opens are attributed to us, keeping StrictMode's untagged-socket detector
* quiet.
*
* `dnsStart`/`connectStart` fire from OkHttp's route-planning and
* `ConnectPlan.connectTcp` on the *same* (TaskRunner) thread that immediately
* goes on to open the DNS/raw socket fd — and they fire for every route type,
* including SOCKS proxies, where OkHttp builds the socket via `Socket(proxy)`
* directly and bypasses the configured SocketFactory. A SocketFactory only
* covers DIRECT routes; an EventListener covers them all.
*/
class TrafficStatsEventListener : EventListener() {
private fun tagThread() {
if (TrafficStats.getThreadStatsTag() == -1) {
TrafficStats.setThreadStatsTag(0x0001)
}
}
override fun dnsStart(call: Call, domainName: String) = tagThread()
override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) = tagThread()
}
@@ -1,9 +1,8 @@
package com.greenart7c3.nostrsigner.service
import com.greenart7c3.nostrsigner.okhttp.TaggedSocketFactory
import com.greenart7c3.nostrsigner.okhttp.TrafficStatsEventListener
import java.io.IOException
import java.util.concurrent.TimeUnit
import javax.net.SocketFactory
import okhttp3.Credentials
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
@@ -17,7 +16,7 @@ object WebDavService {
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.socketFactory(TaggedSocketFactory(SocketFactory.getDefault()))
.eventListener(TrafficStatsEventListener())
.build()
fun uploadFile(