Class SourceThrottle

java.lang.Object
com.iizix.server.user.SourceThrottle

public final class SourceThrottle extends Object
Holds back a client source - the remote address a sign-in attempt comes from - that has failed too often: at most MAX_FAILURES failed attempts per source in a window of WINDOW_MS, after which every attempt from that source is refused until the window ends, whatever the user name. It is the per-source half of the throttle the SignIn survey's D-19 asks for (CC-LOGIN-SERVER-CORE), beside the per-account back-off of UserAuthentication: a stranger who names an account cannot lock it, and a scanner who names many accounts is stopped at its source. It also admits one event per key per interval (acquire(String, long)), for what must not repeat - a password reset mail per account. Modelled on the Mindus License System's RateLimiter (CC-MLS-HARDEN), which lives in another bundle and is not public.

Successful attempts count nothing. Everything is in memory and bounded: a source is held for its window and swept once it has expired (at most once a minute, and whenever a table is full), at most MAX_ENTRIES sources and as many keys are held, and while a table is all live a new source's failure is not recorded and a new key is refused - a flood of distinct addresses costs memory to nobody and holds nobody back for longer than one window. The clock is injected as a monotonic millisecond counter, so that the tests drive time.

What the source is: the client's address as the geolocation resolves it (sourceOf(SocketAddress,ForwardedHeaders,TrustedProxies)): the address of the WebSocket connection as the server sees it, or, when that connection comes from a proxy the server setting $geo.trustedProxies names, the client address that proxy forwarded. Behind an untrusted proxy every client shares the proxy's address, as they share it for geolocation.

An IPv6 client is counted by its /64 network (keyOf(InetAddress)): a host is commonly given a whole /64 and changes its address within it at will, so the address alone would be a new source at every attempt. A link-local address is counted as itself, as every host of every link has the same /64.

A client whose address is unknown (UNKNOWN_SOURCE) is never counted: all such clients would share the one key, and the failures of one would hold back every other (CC-FIX-SIGNIN-THROTTLE-SOURCE). Its failed attempts are counted apart (getUnknownFailures()) and logged at most once a minute; the per-account back-off of UserAuthentication holds for it as for everybody.

Author:
Christopher Mindus
  • Field Details

    • MAX_FAILURES

      public static final int MAX_FAILURES
      The failed attempts a source may make in one window: 20.
      See Also:
    • WINDOW_MS

      public static final long WINDOW_MS
      The window in milliseconds: 15 minutes.
      See Also:
    • MAX_ENTRIES

      public static final int MAX_ENTRIES
      The most sources and keys held at once.
      See Also:
    • UNKNOWN_SOURCE

      public static final String UNKNOWN_SOURCE
      The source of a client whose address is unknown: never held back and never counted per source, see the class description.
      See Also:
  • Constructor Details

    • SourceThrottle

      public SourceThrottle()
      Constructs the throttle on the JVM's monotonic clock.
    • SourceThrottle

      public SourceThrottle(LongSupplier clock)
      Constructs the throttle on a clock.
      Parameters:
      clock - The clock: milliseconds, monotonic, any origin.
  • Method Details

    • sourceOf

      public static String sourceOf(SocketAddress address)
      Gets the source key of a client from its remote address.
      Parameters:
      address - The remote address, null when unknown.
      Returns:
      The source key of the address (keyOf(InetAddress)), or UNKNOWN_SOURCE when there is none.
    • sourceOf

      Gets the source key of a client from its connection, the forwarded-address headers of its upgrade request and the trusted proxies, resolved exactly as the session geolocation resolves the client address (ClientAddressResolver.resolve(InetAddress, ClientAddressResolver.ForwardedHeaders, ClientAddressResolver.TrustedProxies)): the connection's address, unless it is a trusted proxy's and the headers name the client.
      Parameters:
      address - The remote address of the connection, null when unknown.
      headers - The forwarded headers, null for none.
      trusted - The trusted proxies, null for none.
      Returns:
      The source key of the resolved address (keyOf(InetAddress)), or UNKNOWN_SOURCE when there is none.
    • keyOf

      public static String keyOf(InetAddress address)
      Gets the source key of an address: an IPv4 address is its own source; an IPv6 address is counted by its /64 network, written as its first four groups followed by ::/64, except a link-local one, which is its own source without its scope.
      Parameters:
      address - The address, null when unknown.
      Returns:
      The source key, or UNKNOWN_SOURCE when there is no address.
    • heldBack

      public long heldBack(String source)
      Gets how long a source is held back.
      Parameters:
      source - The source key.
      Returns:
      Zero when the source may attempt, otherwise the milliseconds until it may; always zero for a client whose address is unknown.
    • onFailure

      public boolean onFailure(String source)
      Counts one failed attempt of a source.
      Parameters:
      source - The source key.
      Returns:
      true when the source is now held back, false when it may still attempt; always false for a client whose address is unknown, whose failure is counted apart.
    • acquire

      public boolean acquire(String key, long interval)
      Admits one event for a key, or refuses it while the key is held from an earlier event.
      Parameters:
      key - The key, e.g. "reset:" followed by the lower-cased user name.
      interval - The interval in milliseconds the key is held for once admitted.
      Returns:
      true when admitted (the key is now held for the interval), false when refused: the key is held, or it is new and the table holds its maximum of live keys.
    • release

      public void release(String key)
      Gives a key back before its interval is over: for a request that was admitted and then sent nothing (a refused mail relay), so that the next request is not held back by a mail that never went (CC-FIX-SIGNIN-ACCOUNT-MAIL). A key that is not held is left alone.
      Parameters:
      key - The key, as given to acquire(String, long).
    • getUnknownFailures

      public long getUnknownFailures()
      Gets the failed attempts of clients whose address was unknown, which no source was counted for.
      Returns:
      The count since the throttle was constructed.
    • size

      public int size()
      Gets the count of sources and keys held, expired ones included until the next sweep.
      Returns:
      The count.