diff options
| -rw-r--r-- | defaults/yacy.init | 6 | ||||
| -rw-r--r-- | help/AccessTracker_p.md | 8 | ||||
| -rw-r--r-- | source/net/yacy/http/Jetty12HttpServer.java | 26 | ||||
| -rw-r--r-- | source/net/yacy/search/SwitchboardConstants.java | 4 | ||||
| -rw-r--r-- | test/java/net/yacy/http/Jetty12HttpServerTest.java | 43 |
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")); |
