summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Peter Christen <mc@yacy.net>2026-07-12 17:16:24 +0200
committerMichael Peter Christen <mc@yacy.net>2026-07-12 17:16:24 +0200
commitece7985d9435843989c36395c6dd2db2f0b9e933 (patch)
treeb54c2eb1f273a4c716470b5aa90ab67b83b35a4b
parent92e0b111a0ce98964e4de3600d1a81643313ed7e (diff)
proper IP recognition using header X-Real-IP when YaCy is behind a
reverse proxy
-rw-r--r--defaults/yacy.init6
-rw-r--r--help/AccessTracker_p.md8
-rw-r--r--source/net/yacy/http/Jetty12HttpServer.java26
-rw-r--r--source/net/yacy/search/SwitchboardConstants.java4
-rw-r--r--test/java/net/yacy/http/Jetty12HttpServerTest.java43
5 files changed, 84 insertions, 3 deletions
diff --git a/defaults/yacy.init b/defaults/yacy.init
index 545a51780..471a39ec2 100644
--- a/defaults/yacy.init
+++ b/defaults/yacy.init
@@ -30,6 +30,12 @@ upnp.remoteHost =
#iptables -t nat -A PREROUTING -p tcp -s 192.168.24.0/16 --dport 80 -j DNAT --to 192.168.24.1:8090
#(of course you need to customize the ips)
bindPort =
+
+# Comma-separated regular expressions matching trusted reverse-proxy socket IPs.
+# X-Real-IP is used for access tracking only when the direct connection comes
+# from one of these addresses. Authentication and access control always use the
+# direct socket IP. Keep this list restricted to proxies controlled by you.
+server.reverseProxy.trusted=127[.]0[.]0[.]1,0:0:0:0:0:0:0:1,::1
# TLS/SSL support:
#
diff --git a/help/AccessTracker_p.md b/help/AccessTracker_p.md
index 8b3158593..09684e4bc 100644
--- a/help/AccessTracker_p.md
+++ b/help/AccessTracker_p.md
@@ -62,6 +62,14 @@ GET or POST /AccessTracker_p.html?host=...&page=...
Expect observations: counts, logs, queues, timing, network rows, thread states, or resource values. Monitoring does not fix the issue by itself; it points to the next page or setting to change.
+When YaCy is reached through a reverse proxy, Access Tracker uses `X-Real-IP`
+only when the proxy's direct socket IP matches one of the comma-separated regular
+expressions in `server.reverseProxy.trusted`. Loopback proxies are trusted by
+default. Authentication and access-control decisions continue to use the direct
+socket IP, not the forwarded address. The reverse proxy must overwrite
+`X-Real-IP` with the client address; it must not pass a client-supplied value
+unchanged.
+
## Related Pages
- `yacysearch.html`
diff --git a/source/net/yacy/http/Jetty12HttpServer.java b/source/net/yacy/http/Jetty12HttpServer.java
index f38a1a8d0..cc72eab49 100644
--- a/source/net/yacy/http/Jetty12HttpServer.java
+++ b/source/net/yacy/http/Jetty12HttpServer.java
@@ -65,6 +65,8 @@ import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.security.Credential;
import org.eclipse.jetty.util.ssl.SslContextFactory;
+import com.google.common.net.InetAddresses;
+
import net.yacy.cora.protocol.ConnectionInfo;
import net.yacy.cora.protocol.RequestHeader;
import net.yacy.cora.util.ConcurrentLog;
@@ -490,8 +492,11 @@ public class Jetty12HttpServer implements YaCyHttpServer {
protected RoleInfo prepareConstraintInfo(final String pathInContext,
final org.eclipse.jetty.ee8.nested.Request request) {
final Switchboard switchboard = Switchboard.getSwitchboard();
- final String remoteIp = request.getRemoteAddr();
- serverAccessTracker.track(remoteIp, pathInContext);
+ final String socketRemoteIp = request.getRemoteAddr();
+ final String trackingRemoteIp = resolveTrackingClientIp(request,
+ switchboard.getConfig(SwitchboardConstants.SERVER_REVERSE_PROXY_TRUSTED,
+ SwitchboardConstants.SERVER_REVERSE_PROXY_TRUSTED_DEFAULT));
+ serverAccessTracker.track(trackingRemoteIp, pathInContext);
final AdminSecurity.AccessPolicy policy = new AdminSecurity.AccessPolicy(
switchboard.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_All_PAGES, false),
switchboard.isRobinsonMode() && !switchboard.isPublicRobinson(),
@@ -499,7 +504,7 @@ public class Jetty12HttpServer implements YaCyHttpServer {
switchboard.getConfigBool(SwitchboardConstants.ADMIN_ACCOUNT_FOR_LOCALHOST, false),
switchboard.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_USER_NAME, "admin"),
switchboard.getConfig(SwitchboardConstants.ADMIN_ACCOUNT_B64MD5, ""));
- final AdminSecurity.AccessPolicy.Decision decision = policy.decide(pathInContext, remoteIp,
+ final AdminSecurity.AccessPolicy.Decision decision = policy.decide(pathInContext, socketRemoteIp,
request.getHeader(RequestHeader.REFERER),
request.getHeader(RequestHeader.AUTHORIZATION));
if (decision == AdminSecurity.AccessPolicy.Decision.PUBLIC) {
@@ -513,6 +518,21 @@ public class Jetty12HttpServer implements YaCyHttpServer {
roleInfo.addRole(SwitchboardConstants.ADMIN_ACCOUNT_ROLE);
return roleInfo;
}
+
+ /** Resolve the client address for display and tracking, never for access control. */
+ static String resolveTrackingClientIp(final HttpServletRequest request,
+ final String trustedProxyPatterns) {
+ final String socketRemoteIp = request.getRemoteAddr();
+ if (!ProxyAccessPolicy.isClientAllowed(trustedProxyPatterns, socketRemoteIp)) {
+ return socketRemoteIp;
+ }
+ final String forwardedRemoteIp = request.getHeader(RequestHeader.X_Real_IP);
+ if (forwardedRemoteIp == null) {
+ return socketRemoteIp;
+ }
+ final String candidate = forwardedRemoteIp.trim();
+ return InetAddresses.isInetAddress(candidate) ? candidate : socketRemoteIp;
+ }
}
/** Jetty 12 login-service adapter for YaCy's single built-in administrator. */
diff --git a/source/net/yacy/search/SwitchboardConstants.java b/source/net/yacy/search/SwitchboardConstants.java
index 8692c2759..405fbcab8 100644
--- a/source/net/yacy/search/SwitchboardConstants.java
+++ b/source/net/yacy/search/SwitchboardConstants.java
@@ -66,6 +66,10 @@ public final class SwitchboardConstants {
public static final String SERVER_SHUTDOWNPORT = "port.shutdown"; // local port to listen for a shutdown signal (0 <= disabled)
public static final String SERVER_STATICIP = "staticIP"; // static IP of http server
public static final String SERVER_PUBLICPORT = "publicPort";
+ /** Socket peers whose X-Real-IP header may be used for request tracking. */
+ public static final String SERVER_REVERSE_PROXY_TRUSTED = "server.reverseProxy.trusted";
+ public static final String SERVER_REVERSE_PROXY_TRUSTED_DEFAULT =
+ "127[.]0[.]0[.]1,0:0:0:0:0:0:0:1,::1";
public static final String PUBLIC_SEARCHPAGE = "publicSearchpage";
diff --git a/test/java/net/yacy/http/Jetty12HttpServerTest.java b/test/java/net/yacy/http/Jetty12HttpServerTest.java
index 457eaa8ae..f591db7b9 100644
--- a/test/java/net/yacy/http/Jetty12HttpServerTest.java
+++ b/test/java/net/yacy/http/Jetty12HttpServerTest.java
@@ -10,6 +10,7 @@ import static org.junit.Assert.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
+import java.lang.reflect.Proxy;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -42,6 +43,7 @@ import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import net.yacy.cora.order.Digest;
+import net.yacy.cora.protocol.RequestHeader;
import net.yacy.search.SwitchboardConstants;
/**
@@ -115,6 +117,47 @@ public class Jetty12HttpServerTest {
}
@Test
+ public void tracksForwardedClientOnlyForTrustedReverseProxy() {
+ final HttpServletRequest trustedProxyRequest = requestWithRemoteAddress(
+ "127.0.0.1", "198.51.100.23");
+ assertEquals("198.51.100.23",
+ Jetty12HttpServer.AdminSecurityHandler.resolveTrackingClientIp(
+ 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(
+ 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(
+ invalidForwardedAddress,
+ SwitchboardConstants.SERVER_REVERSE_PROXY_TRUSTED_DEFAULT));
+ }
+
+ private static HttpServletRequest requestWithRemoteAddress(final String socketRemoteIp,
+ final String forwardedRemoteIp) {
+ return (HttpServletRequest) Proxy.newProxyInstance(
+ Jetty12HttpServerTest.class.getClassLoader(),
+ new Class<?>[]{HttpServletRequest.class},
+ (proxy, method, args) -> {
+ if ("getRemoteAddr".equals(method.getName())) {
+ return socketRemoteIp;
+ }
+ if ("getHeader".equals(method.getName())
+ && RequestHeader.X_Real_IP.equals(args[0])) {
+ return forwardedRemoteIp;
+ }
+ return null;
+ });
+ }
+
+ @Test
public void preservesJetty9SvgCompressionContract() {
final GzipHandler compression = Jetty12HttpServer.createGzipHandler(null, true);
assertTrue(compression.isMimeTypeDeflatable("image/svg+xml"));