close remaining local dns resolution gaps when classifying hostnames and connecting via tor
What changed, and why it matters
This commit fixes privacy gaps in Sparrow Wallet when it connects through Tor or another proxy. Previously, the app could accidentally ask the computer's normal DNS resolver to translate server names, which could reveal which Bitcoin servers a user was trying to reach. It also could connect to local-network addresses directly even when a proxy was on, potentially bypassing Tor. The patch makes the app treat unknown hostnames as remote when a proxy is active and avoid resolving them locally, and it ensures Tor connections use the proxy for name resolution rather than the local system.
Review and merge. The patch is defensive and privacy-improving. Consider whether any other transports or hostname checks (e.g., non-Tor proxy paths) still perform local DNS resolution when a proxy is configured, and extend the unresolved-address pattern if so. No immediate incident response is indicated.
Security signals we found
DNS leak prevention when proxy/Tor is configured
Avoids local DNS resolution of remote hostnames through configured proxy
Prevents direct local-network connections from bypassing proxy
Uses unresolved socket addresses for Tor proxy resolution
Adds unit tests for proxy-aware local address classification
Evidence from the diff
The change closes two local DNS-leak paths. IpAddressMatcher.isLocalNetworkAddress() now refuses to resolve non-IP, non-mDNS hostnames when AppServices.isUsingProxy() is true, returning false so the connection is treated as remote and routed through the proxy. IP literals and *.local mDNS names are still allowed to be classified as local. TorTcpTransport.createSocket() now uses InetSocketAddress.createUnresolved() so the SOCKS/Tor proxy performs the hostname resolution instead of the JVM performing local DNS before connecting. Tests are added covering proxy-on/off behavior and hostname assumptions.
Changed components
com.sparrowwallet.sparrow.net.IpAddressMatchercom.sparrowwallet.sparrow.net.TorTcpTransportSparrow Wallet proxy/Tor connection logicInspect captured patch +80 / −2
### src/main/java/com/sparrowwallet/sparrow/net/IpAddressMatcher.java
@@ -16,12 +16,14 @@
* limitations under the License.
*/
+import com.google.common.net.InetAddresses;
import com.sparrowwallet.sparrow.AppServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.util.Locale;
/**
* Matches a request based on IP Address or subnet mask matching against the remote
@@ -109,7 +111,18 @@ private InetAddress parseAddress(String address) {
public static boolean isLocalNetworkAddress(String address) {
try {
- return "localhost".equals(address) || "127.0.0.1".equals(address) || LOCAL_RANGE_1.matches(address) || LOCAL_RANGE_2.matches(address) || LOCAL_RANGE_3.matches(address) || LOCAL_RANGE_4.matches(address);
+ if("localhost".equals(address) || "127.0.0.1".equals(address)) {
+ return true;
+ }
+
+ //Matching a hostname against the local ranges requires resolving it, which leaks the name to (and trusts the answer of) the local DNS resolver even when a proxy is configured
+ //Only IP literals and mDNS names (which RFC 6762 requires to be resolved via link-local multicast, not upstream DNS) are considered potentially local when using a proxy
+ if(AppServices.isUsingProxy() && !InetAddresses.isInetAddress(address) && !address.toLowerCase(Locale.ROOT).endsWith(".local")) {
+ log.info("Avoiding local DNS resolution of " + address + ", assuming it is a non-local address to be resolved by the configured proxy");
+ return false;
+ }
+
+ return LOCAL_RANGE_1.matches(address) || LOCAL_RANGE_2.matches(address) || LOCAL_RANGE_3.matches(address) || LOCAL_RANGE_4.matches(address);
} catch(IllegalArgumentException e) {
if(AppServices.isUsingProxy()) {
log.info(e.getMessage() + ", assuming it is a non-local address to be resolved by the configured proxy");
### src/main/java/com/sparrowwallet/sparrow/net/TorTcpTransport.java
@@ -23,6 +23,6 @@ protected void createSocket() throws IOException {
}
socket = new Socket(Tor.getDefault().getProxy());
- socket.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(getDefaultPort())));
+ socket.connect(InetSocketAddress.createUnresolved(server.getHost(), server.getPortOrDefault(getDefaultPort())));
}
}
### src/test/java/com/sparrowwallet/sparrow/net/IpAddressMatcherTest.java
@@ -0,0 +1,65 @@
+package com.sparrowwallet.sparrow.net;
+
+import com.sparrowwallet.drongo.Network;
+import com.sparrowwallet.sparrow.SparrowWallet;
+import com.sparrowwallet.sparrow.io.Config;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Path;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class IpAddressMatcherTest {
+ @TempDir
+ private static Path tempHome;
+
+ @BeforeAll
+ public static void setUp() {
+ //Isolate Config.get() from the developer's config so the proxy setting can be changed safely
+ System.setProperty(SparrowWallet.APP_HOME_PROPERTY, tempHome.toString());
+ Network.set(Network.MAINNET);
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ Config.get().setUseProxy(false);
+ System.clearProperty(SparrowWallet.APP_HOME_PROPERTY);
+ }
+
+ @Test
+ public void classifiesAddressesWithoutProxy() {
+ Config.get().setUseProxy(false);
+ assertTrue(IpAddressMatcher.isLocalNetworkAddress("localhost"));
+ assertTrue(IpAddressMatcher.isLocalNetworkAddress("127.0.0.1"));
+ assertTrue(IpAddressMatcher.isLocalNetworkAddress("192.168.1.5"));
+ assertTrue(IpAddressMatcher.isLocalNetworkAddress("10.0.0.10"));
+ assertTrue(IpAddressMatcher.isLocalNetworkAddress("172.16.0.1"));
+ assertTrue(IpAddressMatcher.isLocalNetworkAddress("100.64.0.1"));
+ assertFalse(IpAddressMatcher.isLocalNetworkAddress("8.8.8.8"));
+ }
+
+ @Test
+ public void classifiesIpLiteralsWithProxy() {
+ Config.get().setUseProxy(true);
+ //IP literals classify without DNS resolution, so local network servers must still connect directly when a proxy is configured
+ assertTrue(IpAddressMatcher.isLocalNetworkAddress("localhost"));
+ assertTrue(IpAddressMatcher.isLocalNetworkAddress("127.0.0.1"));
+ assertTrue(IpAddressMatcher.isLocalNetworkAddress("192.168.1.5"));
+ assertTrue(IpAddressMatcher.isLocalNetworkAddress("10.0.0.10"));
+ assertTrue(IpAddressMatcher.isLocalNetworkAddress("172.16.0.1"));
+ assertTrue(IpAddressMatcher.isLocalNetworkAddress("100.64.0.1"));
+ assertFalse(IpAddressMatcher.isLocalNetworkAddress("8.8.8.8"));
+ }
+
+ @Test
+ public void assumesHostnamesAreRemoteWithProxy() {
+ Config.get().setUseProxy(true);
+ //Hostnames are not resolved by the local DNS resolver when a proxy is configured, and are assumed to be remote addresses resolved by the proxy
+ assertFalse(IpAddressMatcher.isLocalNetworkAddress("electrumx.example.com"));
+ assertFalse(IpAddressMatcher.isLocalNetworkAddress("notarealhost.invalid"));
+ }
+}Why this scored 65/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.