Class ClientIpResolver

java.lang.Object
org.frontcache.core.ClientIpResolver

public class ClientIpResolver extends Object
Resolves the client IP address that rate limiting counts against. This is deliberately NOT FCUtils.getClientIP(HttpServletRequest). That method walks thirteen candidate headers and returns the first non-empty one, which is right for a best-effort log column and unusable as a limiter key: every one of those headers is client-supplied, so a client sending "X-Forwarded-For: <random>" on each request would get a fresh counter on each request and defeat the limiter entirely - while passing every test that does not try it. The two resolutions coexist on purpose; see docs/archive/guard-rate-limit-proposal.md ยง3. The rules here:
  • A forwarding header is read ONLY when the socket peer is a configured trusted proxy. Otherwise the peer address itself is the key and every header is ignored.
  • The header is walked RIGHT TO LEFT, returning the first entry that is not itself a trusted proxy. The leftmost entry - which most implementations use - is whatever the client wrote; the rightmost non-trusted entry is what the last proxy we trust actually observed.
  • The result is canonicalized so that the same address always produces the same key: IPv6 literals are expanded to one textual form, and IPv4-mapped IPv6 (::ffff:203.0.113.7) folds to its IPv4 form so a dual-stack container does not split one client in two.
  • No prefix, no subnet, no CIDR grouping: one address is one counter. The consequence - an IPv6 client holding a /64 can rotate addresses and evade the limit - is accepted and documented, in exchange for never limiting a shared-NAT office because of one user behind it.
Never throws: an address that cannot be resolved returns null, and the caller declines to meter that request rather than failing it.
  • Field Details

  • Constructor Details

    • ClientIpResolver

      public ClientIpResolver(String trustedProxiesCsv, String headerName)
  • Method Details

    • getInstance

      public static ClientIpResolver getInstance()
      Returns:
      the resolver built from the current configuration, created on first use
    • reload

      public static void reload()
      Re-reads the configuration. Called from GuardRuleEngine.load(), so an edited frontcache.properties is picked up by the next engine reload rather than only by a restart.
    • install

      public static void install(ClientIpResolver resolver)
      for tests - installs a resolver without going through FCConfig
    • hasTrustedProxies

      public boolean hasTrustedProxies()
      Returns:
      true when at least one trusted proxy is configured. Rate limiting behind a reverse proxy with none configured keys every request on the proxy's own address, so the guard engine warns about it at load time.
    • isTrustedPeer

      public boolean isTrustedPeer(jakarta.servlet.http.HttpServletRequest request)
      Whether the socket peer of this request is a configured trusted proxy - i.e. whether the forwarding headers it carries were written by something we control. Exposed for X-Forwarded-Proto, which is subject to exactly the same rule as X-Forwarded-For and for the same reason: the scheme ends up in a redirect a browser follows, so a client must not be able to choose it by sending a header. Kept here rather than reimplemented at the call site so there is one definition of "trusted" and one CIDR list.
      Parameters:
      request - current request; may be null or container-recycled
      Returns:
      true only when the peer address parses and falls inside a configured CIDR
    • resolve

      public String resolve(jakarta.servlet.http.HttpServletRequest request)
      Parameters:
      request - current request; may be null or container-recycled
      Returns:
      canonical client address, or null when it cannot be established