From c729aef849f06c7dce64f1ee243f018347dbc144 Mon Sep 17 00:00:00 2001 From: Michael Peter Christen Date: Tue, 14 Jul 2026 22:18:11 +0200 Subject: Server-validated X-Real-IP proxy handling, publicPort lifecycle fix, peer-statistic iam parameter, transfer-IP fallback, and matching tests/docs --- .../net/yacy/cora/protocol/RequestHeaderTest.java | 41 +++++++---- .../yacy/htroot/yacy/SearchPeerResolutionTest.java | 79 ++++++++++++++++++++++ test/java/net/yacy/http/Jetty12HttpServerTest.java | 51 +++++++++++++- test/java/net/yacy/peers/ProtocolTest.java | 45 ++++++++++++ test/java/net/yacy/server/serverSwitchTest.java | 66 ++++++++++++++++++ 5 files changed, 264 insertions(+), 18 deletions(-) create mode 100644 test/java/net/yacy/htroot/yacy/SearchPeerResolutionTest.java create mode 100644 test/java/net/yacy/peers/ProtocolTest.java create mode 100644 test/java/net/yacy/server/serverSwitchTest.java (limited to 'test') diff --git a/test/java/net/yacy/cora/protocol/RequestHeaderTest.java b/test/java/net/yacy/cora/protocol/RequestHeaderTest.java index 0931d8e0c..6b953971b 100644 --- a/test/java/net/yacy/cora/protocol/RequestHeaderTest.java +++ b/test/java/net/yacy/cora/protocol/RequestHeaderTest.java @@ -37,9 +37,10 @@ public class RequestHeaderTest { /** * Build a minimal HttpServletRequest stub answering getRemoteAddr() with the - * given socket peer address and returning the given X-Real-IP header value. - */ - private static HttpServletRequest stubRequest(final String socketPeer, final String xRealIP) { + * given socket peer address, X-Real-IP header and server-validated effective IP. + */ + private static HttpServletRequest stubRequest(final String socketPeer, final String xRealIP, + final String effectiveClientIp) { final InvocationHandler h = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) { @@ -48,8 +49,11 @@ public class RequestHeaderTest { return socketPeer; case "getRemoteHost": return socketPeer; - case "getHeader": - return RequestHeader.X_Real_IP.equals(args[0]) ? xRealIP : null; + case "getHeader": + return RequestHeader.X_Real_IP.equals(args[0]) ? xRealIP : null; + case "getAttribute": + return RequestHeader.EFFECTIVE_CLIENT_IP_ATTRIBUTE.equals(args[0]) + ? effectiveClientIp : null; default: return null; } @@ -70,20 +74,27 @@ public class RequestHeaderTest { final String remoteClient = "203.0.113.7"; // a non-local address (TEST-NET-3) // spoofing attempt: remote socket peer, but X-Real-IP claims localhost - final RequestHeader spoofed = new RequestHeader(stubRequest(remoteClient, "127.0.0.1")); - // routing accessor honors the header (kept for peer routing behind a trusted proxy) ... - assertEquals("127.0.0.1", spoofed.getRemoteAddr()); - // ... but the authentication accessor must return the true socket peer - assertEquals(remoteClient, spoofed.getRemoteSocketAddr()); - assertFalse("spoofed X-Real-IP must not grant localhost access", spoofed.accessFromLocalhost()); - - // genuine localhost access still works - final RequestHeader local = new RequestHeader(stubRequest("127.0.0.1", null)); + final RequestHeader spoofed = new RequestHeader( + stubRequest(remoteClient, "127.0.0.1", null)); + // Neither routing nor authentication may honor an unvalidated forwarding header. + assertEquals(remoteClient, spoofed.getRemoteAddr()); + assertEquals(remoteClient, spoofed.getRemoteSocketAddr()); + assertFalse("spoofed X-Real-IP must not grant localhost access", spoofed.accessFromLocalhost()); + + // A trusted HTTP-server decision is visible to P2P routing, but not socket security. + final RequestHeader proxied = new RequestHeader( + stubRequest("127.0.0.1", "198.51.100.23", "198.51.100.23")); + assertEquals("198.51.100.23", proxied.getRemoteAddr()); + assertEquals("198.51.100.23", proxied.getRemoteHost()); + assertEquals("127.0.0.1", proxied.getRemoteSocketAddr()); + + // genuine localhost access still works + final RequestHeader local = new RequestHeader(stubRequest("127.0.0.1", null, null)); assertEquals("127.0.0.1", local.getRemoteSocketAddr()); assertTrue(local.accessFromLocalhost()); // remote client without spoofing stays remote - final RequestHeader remote = new RequestHeader(stubRequest(remoteClient, null)); + final RequestHeader remote = new RequestHeader(stubRequest(remoteClient, null, null)); assertFalse(remote.accessFromLocalhost()); } diff --git a/test/java/net/yacy/htroot/yacy/SearchPeerResolutionTest.java b/test/java/net/yacy/htroot/yacy/SearchPeerResolutionTest.java new file mode 100644 index 000000000..4f1359f26 --- /dev/null +++ b/test/java/net/yacy/htroot/yacy/SearchPeerResolutionTest.java @@ -0,0 +1,79 @@ +package net.yacy.htroot.yacy; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import net.yacy.peers.Seed; +import net.yacy.peers.SeedDB; + +public class SearchPeerResolutionTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private SeedDB seedDB; + + @Before + public void setUp() throws Exception { + final File networkRoot = this.temporaryFolder.newFolder("network"); + this.seedDB = new SeedDB( + networkRoot, + "seedActive.db", + "seedPassive.db", + "seedPotential.db", + new File(networkRoot, SeedDB.DBFILE_OWN_SEED), + 1, + 4, + false, + false); + } + + @After + public void tearDown() { + if (this.seedDB != null) this.seedDB.close(); + } + + @Test + public void iamHashDisambiguatesPeersSharingOneNatAddress() throws Exception { + final Seed firstPeer = seed("AAAAAAAAAAAA", "first-nat-peer", "198.51.100.10", 8090); + final Seed secondPeer = seed("BBBBBBBBBBBB", "second-nat-peer", "198.51.100.10", 8091); + this.seedDB.addConnected(firstPeer); + this.seedDB.addConnected(secondPeer); + + final Seed resolved = search.resolveRemotePeer(this.seedDB, secondPeer.hash, "198.51.100.10"); + + assertEquals(secondPeer.hash, resolved.hash); + } + + @Test + public void unknownOrInvalidIamHashFallsBackToClientIp() throws Exception { + final Seed peer = seed("CCCCCCCCCCCC", "fallback-peer", "198.51.100.20", 8090); + this.seedDB.addConnected(peer); + + assertEquals(peer.hash, + search.resolveRemotePeer(this.seedDB, "DDDDDDDDDDDD", "198.51.100.20").hash); + assertEquals(peer.hash, + search.resolveRemotePeer(this.seedDB, "not-a-hash", "198.51.100.20").hash); + assertEquals(peer.hash, + search.resolveRemotePeer(this.seedDB, "", "198.51.100.20").hash); + } + + private static Seed seed(final String hash, final String name, final String ip, final int port) { + final ConcurrentMap dna = new ConcurrentHashMap(); + final Seed seed = new Seed(hash, dna); + seed.setName(name); + seed.setType(Seed.PEERTYPE_SENIOR); + seed.setIP(ip); + seed.setPort(port); + return seed; + } +} diff --git a/test/java/net/yacy/http/Jetty12HttpServerTest.java b/test/java/net/yacy/http/Jetty12HttpServerTest.java index f591db7b9..39cf4e380 100644 --- a/test/java/net/yacy/http/Jetty12HttpServerTest.java +++ b/test/java/net/yacy/http/Jetty12HttpServerTest.java @@ -13,6 +13,8 @@ import java.io.OutputStream; import java.lang.reflect.Proxy; import java.net.Socket; import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; @@ -121,27 +123,60 @@ public class Jetty12HttpServerTest { final HttpServletRequest trustedProxyRequest = requestWithRemoteAddress( "127.0.0.1", "198.51.100.23"); assertEquals("198.51.100.23", - Jetty12HttpServer.AdminSecurityHandler.resolveTrackingClientIp( + Jetty12HttpServer.AdminSecurityHandler.resolveTrustedClientIp( trustedProxyRequest, SwitchboardConstants.SERVER_REVERSE_PROXY_TRUSTED_DEFAULT)); final HttpServletRequest spoofedDirectRequest = requestWithRemoteAddress( "203.0.113.10", "127.0.0.1"); assertEquals("203.0.113.10", - Jetty12HttpServer.AdminSecurityHandler.resolveTrackingClientIp( + Jetty12HttpServer.AdminSecurityHandler.resolveTrustedClientIp( spoofedDirectRequest, SwitchboardConstants.SERVER_REVERSE_PROXY_TRUSTED_DEFAULT)); final HttpServletRequest invalidForwardedAddress = requestWithRemoteAddress( "127.0.0.1", "unknown, 198.51.100.23"); assertEquals("127.0.0.1", - Jetty12HttpServer.AdminSecurityHandler.resolveTrackingClientIp( + Jetty12HttpServer.AdminSecurityHandler.resolveTrustedClientIp( invalidForwardedAddress, SwitchboardConstants.SERVER_REVERSE_PROXY_TRUSTED_DEFAULT)); + + final HttpServletRequest trustedProxyIpv6Request = requestWithRemoteAddress( + "::1", "2001:db8::23"); + assertEquals("2001:db8::23", + Jetty12HttpServer.AdminSecurityHandler.resolveTrustedClientIp( + trustedProxyIpv6Request, + SwitchboardConstants.SERVER_REVERSE_PROXY_TRUSTED_DEFAULT)); + } + + @Test + public void exposesOnlyServerValidatedProxyAddressToRequestHeader() { + final HttpServletRequest trustedProxyRequest = requestWithRemoteAddress( + "127.0.0.1", "198.51.100.23"); + trustedProxyRequest.setAttribute(RequestHeader.EFFECTIVE_CLIENT_IP_ATTRIBUTE, + Jetty12HttpServer.AdminSecurityHandler.resolveTrustedClientIp( + trustedProxyRequest, + SwitchboardConstants.SERVER_REVERSE_PROXY_TRUSTED_DEFAULT)); + + final RequestHeader trustedProxyHeader = new RequestHeader(trustedProxyRequest); + assertEquals("198.51.100.23", trustedProxyHeader.getRemoteAddr()); + assertEquals("127.0.0.1", trustedProxyHeader.getRemoteSocketAddr()); + + final HttpServletRequest spoofedDirectRequest = requestWithRemoteAddress( + "203.0.113.10", "127.0.0.1"); + spoofedDirectRequest.setAttribute(RequestHeader.EFFECTIVE_CLIENT_IP_ATTRIBUTE, + Jetty12HttpServer.AdminSecurityHandler.resolveTrustedClientIp( + spoofedDirectRequest, + SwitchboardConstants.SERVER_REVERSE_PROXY_TRUSTED_DEFAULT)); + + final RequestHeader spoofedDirectHeader = new RequestHeader(spoofedDirectRequest); + assertEquals("203.0.113.10", spoofedDirectHeader.getRemoteAddr()); + assertEquals("203.0.113.10", spoofedDirectHeader.getRemoteSocketAddr()); } private static HttpServletRequest requestWithRemoteAddress(final String socketRemoteIp, final String forwardedRemoteIp) { + final Map attributes = new HashMap(); return (HttpServletRequest) Proxy.newProxyInstance( Jetty12HttpServerTest.class.getClassLoader(), new Class[]{HttpServletRequest.class}, @@ -149,10 +184,20 @@ public class Jetty12HttpServerTest { if ("getRemoteAddr".equals(method.getName())) { return socketRemoteIp; } + if ("getRemoteHost".equals(method.getName())) { + return socketRemoteIp; + } if ("getHeader".equals(method.getName()) && RequestHeader.X_Real_IP.equals(args[0])) { return forwardedRemoteIp; } + if ("setAttribute".equals(method.getName())) { + attributes.put((String) args[0], args[1]); + return null; + } + if ("getAttribute".equals(method.getName())) { + return attributes.get(args[0]); + } return null; }); } diff --git a/test/java/net/yacy/peers/ProtocolTest.java b/test/java/net/yacy/peers/ProtocolTest.java new file mode 100644 index 000000000..9c994ca38 --- /dev/null +++ b/test/java/net/yacy/peers/ProtocolTest.java @@ -0,0 +1,45 @@ +package net.yacy.peers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.junit.Test; + +public class ProtocolTest { + + @Test + public void transferTargetIpUsesRecordedAttemptAddress() { + final Seed seed = seed(); + seed.setIP("192.0.2.10"); + seed.setIP("2001:db8::10"); + final Map response = new HashMap(); + response.put(Seed.IP, "2001:db8::10"); + + assertEquals("2001:db8::10", Protocol.transferTargetIP(response, seed)); + } + + @Test + public void transferTargetIpFallsBackToCanonicalIpv6OnlyAddress() { + final Seed seed = seed(); + seed.put(Seed.IP, ""); + seed.put(Seed.IP6, "2001:db8::20"); + + assertEquals("2001:db8::20", Protocol.transferTargetIP(Collections.emptyMap(), seed)); + } + + @Test + public void transferTargetIpIsNullWhenPeerHasNoAddress() { + assertNull(Protocol.transferTargetIP(Collections.emptyMap(), seed())); + } + + private static Seed seed() { + final ConcurrentMap dna = new ConcurrentHashMap(); + return new Seed("testseedhash", dna); + } +} diff --git a/test/java/net/yacy/server/serverSwitchTest.java b/test/java/net/yacy/server/serverSwitchTest.java new file mode 100644 index 000000000..4d6efe044 --- /dev/null +++ b/test/java/net/yacy/server/serverSwitchTest.java @@ -0,0 +1,66 @@ +package net.yacy.server; + +import static org.junit.Assert.assertEquals; + +import java.io.File; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import net.yacy.search.SwitchboardConstants; + +public class serverSwitchTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void publicPortOverridesUpnpAndLocalPortAndSurvivesReload() throws Exception { + final File root = this.temporaryFolder.newFolder("peer"); + serverSwitch peer = new serverSwitch(root, root, "missing.init", "settings/yacy.conf"); + peer.setConfig(SwitchboardConstants.SERVER_PORT, "8090"); + peer.setConfig(SwitchboardConstants.SERVER_SSLPORT, "8443"); + peer.setConfig(SwitchboardConstants.SERVER_PUBLICPORT, "443"); + peer.setConnectedViaUpnp(true); + peer.setUpnpPorts(SwitchboardConstants.SERVER_PORT, 18090); + peer.setUpnpPorts(SwitchboardConstants.SERVER_SSLPORT, 18443); + + assertEquals(443, peer.getPublicPort(SwitchboardConstants.SERVER_PORT, 8090)); + assertEquals(18443, peer.getPublicPort(SwitchboardConstants.SERVER_SSLPORT, 8443)); + + peer = new serverSwitch(root, root, "missing.init", "settings/yacy.conf"); + assertEquals(443, peer.getPublicPort(SwitchboardConstants.SERVER_PORT, 8090)); + } + + @Test + public void missingOrInvalidPublicPortFallsBackToUpnpThenLocalPort() throws Exception { + final File root = this.temporaryFolder.newFolder("fallback-peer"); + serverSwitch peer = + new serverSwitch(root, root, "missing.init", "settings/yacy.conf"); + peer.setConfig(SwitchboardConstants.SERVER_PORT, "8090"); + peer.setConnectedViaUpnp(true); + peer.setUpnpPorts(SwitchboardConstants.SERVER_PORT, 18090); + + peer.setConfig(SwitchboardConstants.SERVER_PUBLICPORT, "443"); + assertEquals(443, peer.getPublicPort(SwitchboardConstants.SERVER_PORT, 8090)); + + peer.setConfig(SwitchboardConstants.SERVER_PUBLICPORT, ""); + assertEquals(18090, peer.getPublicPort(SwitchboardConstants.SERVER_PORT, 8090)); + + peer.setConfig(SwitchboardConstants.SERVER_PUBLICPORT, "0"); + assertEquals(18090, peer.getPublicPort(SwitchboardConstants.SERVER_PORT, 8090)); + + peer.setConfig(SwitchboardConstants.SERVER_PUBLICPORT, "65535"); + assertEquals(65535, peer.getPublicPort(SwitchboardConstants.SERVER_PORT, 8090)); + + peer.setConfig(SwitchboardConstants.SERVER_PUBLICPORT, "65536"); + assertEquals(18090, peer.getPublicPort(SwitchboardConstants.SERVER_PORT, 8090)); + + peer.setConnectedViaUpnp(false); + assertEquals(8090, peer.getPublicPort(SwitchboardConstants.SERVER_PORT, 8090)); + + peer = new serverSwitch(root, root, "missing.init", "settings/yacy.conf"); + assertEquals(8090, peer.getPublicPort(SwitchboardConstants.SERVER_PORT, 8090)); + } +} -- cgit v1.2.3