Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java
Original file line number Diff line number Diff line change
Expand Up @@ -2819,11 +2819,13 @@ public void onStatsInteractiveStateChanged(boolean interactive) {
}
});

// On screen-off, drop idle DoH keep-alive sockets so a
// server-side reset during doze can't wake the radio.
if (!last_interactive)
net.kollnig.missioncontrol.dns.DnsProxyServer
.getInstance(ServiceSinkhole.this).onScreenOff();
// Screen state gates the DoH battery policy: while the
// screen is off the proxy drops retries and idle
// keep-alive sockets so a server-side reset during doze
// can't wake the radio.
net.kollnig.missioncontrol.dns.DnsProxyServer
.getInstance(ServiceSinkhole.this)
.onScreenStateChanged(last_interactive);
} catch (Throwable ex) {
Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ public class DnsOverHttpsClient {
});
private static DnsOverHttpsClient instance;
private static Cache responseCache;
// Screen-off DoH battery policy: while the device is dozing we skip retries
// and evict keep-alive sockets so a server-side reset can't wake the radio.
private static volatile boolean screenOff = false;
private final OkHttpClient client;
private final String endpoint;

Expand Down Expand Up @@ -134,13 +137,14 @@ public static synchronized void resetInstance() {
}

/**
* Evict idle keep-alive connections from the current client, if any. Called on
* screen-off so an idle pooled TLS socket cannot be reset by the server during
* doze and wake the radio. In-flight requests are unaffected. No-op if no
* client has been created yet.
* Apply the screen-state DoH battery policy. While the screen is off the
* client drops retries and evicts keep-alive connections so an idle pooled
* TLS socket cannot be reset by the server during doze and wake the radio.
* In-flight requests are unaffected. No-op if no client has been created yet.
*/
public static synchronized void evictIdleConnections() {
if (instance != null) {
public static synchronized void setScreenOff(boolean off) {
screenOff = off;
if (off && instance != null) {
instance.evictIdle();
}
}
Expand Down Expand Up @@ -191,7 +195,11 @@ public byte[] resolve(@NonNull byte[] dnsQuery) {

Request request = buildRequest(endpoint, dnsQuery);

for (int attempt = 0; attempt <= MAX_RETRIES; attempt++) {
// Screen off: do not retry. A second round trip would double the radio
// wakeups during doze for a query that is already failing.
int maxRetries = screenOff ? 0 : MAX_RETRIES;

for (int attempt = 0; attempt <= maxRetries; attempt++) {
if (attempt > 0) {
try {
Thread.sleep(RETRY_DELAY_MS);
Expand Down Expand Up @@ -228,6 +236,13 @@ public byte[] resolve(@NonNull byte[] dnsQuery) {
}
} catch (IOException e) {
Log.e(TAG, "DoH request failed: " + e.getMessage());
} finally {
// Screen off: never leave an idle keep-alive socket behind — a
// server-side reset during doze would wake the radio. Cache
// hits are unaffected (no connection is created for them).
if (screenOff) {
evictIdle();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ public synchronized void start() {
// Start the main listener thread
new Thread(this::runServer, "DnsProxyServer").start();

// Sync the screen-state policy so a start mid-doze (e.g. a network
// reload at night) doesn't inherit the screen-on behaviour.
DnsOverHttpsClient.setScreenOff(!eu.faircode.netguard.Util.isInteractive(context));

Log.i(TAG, "DNS proxy server started on " + DNS_PROXY_ADDRESS + ":" + DNS_PROXY_PORT);

// Start TCP server only if enabled (still in testing)
Expand Down Expand Up @@ -166,16 +170,17 @@ public synchronized void stop() {
}

/**
* Called when the screen turns off. Evicts idle keep-alive HTTPS connections
* so an idle pooled TLS socket can't be reset by the server mid-doze and wake
* the radio. In-flight requests keep their connections. The response cache is
* intentionally preserved — it is most valuable precisely while the screen is
* off. No-op when the proxy is not running.
* Apply the screen-state DoH battery policy. While the screen is off the
* DoH client drops retries and evicts idle keep-alive HTTPS connections so
* a server-side reset mid-doze can't wake the radio. In-flight requests
* keep their connections; the response cache is intentionally preserved —
* it is most valuable precisely while the screen is off. No-op when the
* proxy is not running.
*/
public void onScreenOff() {
public void onScreenStateChanged(boolean interactive) {
if (!running.get())
return;
DnsOverHttpsClient.evictIdleConnections();
DnsOverHttpsClient.setScreenOff(!interactive);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,15 @@ public class DnsOverHttpsClientTest {

@Before
public void setUp() throws IOException {
DnsOverHttpsClient.setScreenOff(false);
DnsOverHttpsClient.resetInstance();
server = new MockWebServer();
server.start();
}

@After
public void tearDown() throws IOException {
DnsOverHttpsClient.setScreenOff(false);
DnsOverHttpsClient.resetInstance();
server.close();
}
Expand Down Expand Up @@ -102,6 +104,22 @@ public void resolveRetriesServerErrorsThenGivesUp() {
assertEquals(3, server.getRequestCount());
}

@Test
public void resolveDoesNotRetryWhenScreenOff() {
DnsOverHttpsClient.setScreenOff(true);
try {
server.enqueue(dnsResponse(503, new byte[0]));

assertNull(client().resolve(QUERY));

// A retry would be a second radio wakeup during doze; screen-off
// resolution must give up after the first failed round trip.
assertEquals(1, server.getRequestCount());
} finally {
DnsOverHttpsClient.setScreenOff(false);
}
}

@Test
public void resolveRetriesInvalidShortDnsResponse() {
server.enqueue(dnsResponse(200, new byte[] { 1, 2, 3 }));
Expand Down