JSR-356 WebSockets with Tomcat - How to limit connections within single IP address?
Asked Answered
B

5

7

I made a JSR-356 @ServerEndpoint in which I want to limit alive connections from single IP address, to prevent simple DDOS attacks.

Note that I'm search for Java solution (JSR-356, Tomcat or Servlet 3.0 specs).

I have tried custom endpoint configurer but I don't have access to IP address even in HandshakeRequest object.

How to limit JSR-356 connection count from single IP address without external software like iptables?

Brigidabrigit answered 5/4, 2014 at 11:8 Comment(3)
Don't write code for this. Use the firewall. By the time you get into your Java code it is already too late.Simmers
@EJP As question states - need java solution. I'm not asking for best solution. Suppose we want max simplicity and portability in product shipped to lot of environments. It is very easy to make 40 000 connections from single host, but disconnecting automatically will make this a lot harder. Tomcat stucks with max connections on websockets, so if it is configured to handle ~200k connection I just want to prevent very easy service blocking with just holding connections, which not need lot of resources like botnet.Charie
You're assuming that there is a Java solution, without proof. If you're shipping a product or have other constraints you should have stated them in your question. Most people aren't. And as a matter of fact it isn't very easy to make 40,000 connections from a single host. Many environments won't go beyond a few thousand.Simmers
O
13

According to Tomcat developer @mark-thomas client IP is not exposed via JSR-356 thus it is impossible to implement such a function with pure JSR-356 API-s.

You have to use a rather ugly hack to work around the limitation of the standard.

What needs to be done boils down to:

  1. Generate each user a token that contains their IP on initial request (before websocket handshake)
  2. Pass the token down the chain until it reaches endpoint implementation

There are at least two hacky options to achieve that.

Use HttpSession

  1. Listen to incoming HTTP requests with a ServletRequestListener
  2. Call request.getSession() on incoming request to ensure it has a session and store client IP as a session attribute.
  3. Create a ServerEndpointConfig.Configurator that lifts client IP from HandshakeRequest#getHttpSession and attaches it to EndpointConfig as a user property using the modifyHandshake method.
  4. Get the client IP from EndpointConfig user properties, store it in map or whatever and trigger cleanup logic if the number of sessions per IP exceeds a threshold.

You can also use a @WebFilter instead of ServletRequestListener

Note that this option can have a high resource consumption unless your application already uses sessions e.g. for authentication purposes.

Pass IP as an encrypted token in the URL

  1. Create a servlet or a filter that attaches to a non websocket entry point. e.g. /mychat
  2. Get client IP, encrypt it with a random salt and a secret key to generate a token.
  3. Use ServletRequest#getRequestDispatcher to forward the request to /mychat/TOKEN
  4. Configure your endpoint to use path parameters e.g. @ServerEndpoint("/mychat/{token}")
  5. Lift the token from @PathParam and decrypt to get client IP. Store it in map or whatever and trigger cleanup logic if the number of sessions per IP exceeds a threshold.

For ease of installation you may wish to generate encryption keys on application startup.

Please note that you need to encrypt the IP even if you are doing an internal dispatch that is not visible to the client. There is nothing that would stop an attacker from connecting to /mychat/2.3.4.5 directly thus spoofing the client IP if it's not encrypted.

See also:

Ordovician answered 12/4, 2014 at 1:37 Comment(3)
Are you sure that your second suggestion works? Perhaps you can provide a gist? At least I'm getting a 500 error when I'm trying to forward an upgrade request in Jetty 9.3.6.v20151106. Connecting to the websocket directly works fine.Anthotaxy
To answer my own comments, the second suggestion does not work at all and it is not possible to use filters in the first option, but using a listener works. See #24914508 for more info.Anthotaxy
To correct my earlier comment - using filter does work in Tomcat (7/8) but not in Jetty. There's a bug open against the spec to clarify whether using (servlet) filters should work or not, see more at #26104439Anthotaxy
W
4

the socket object is hidden in WsSession, so you can use reflection to got the ip address. the execution time of this method is about 1ms. this solution is not prefect but useful.

public static InetSocketAddress getRemoteAddress(WsSession session) {
    if(session == null){
        return null;
    }

    Async async = session.getAsyncRemote();
    InetSocketAddress addr = (InetSocketAddress) getFieldInstance(async, 
            "base#sos#socketWrapper#socket#sc#remoteAddress");

    return addr;
}

private static Object getFieldInstance(Object obj, String fieldPath) {
    String fields[] = fieldPath.split("#");
    for(String field : fields) {
        obj = getField(obj, obj.getClass(), field);
        if(obj == null) {
            return null;
        }
    }

    return obj;
}

private static Object getField(Object obj, Class<?> clazz, String fieldName) {
    for(;clazz != Object.class; clazz = clazz.getSuperclass()) {
        try {
            Field field;
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(obj);
        } catch (Exception e) {
        }            
    }

    return null;
}

and the pom config is

<dependency>
  <groupId>javax.websocket</groupId>
  <artifactId>javax.websocket-all</artifactId>
  <version>1.1</version>
  <type>pom</type>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat-websocket</artifactId>
  <version>8.0.26</version>
  <scope>provided</scope>
</dependency>
Walli answered 17/9, 2015 at 11:47 Comment(4)
Thanks for posting this workaround. This works on the Tomcat 8.0.x versions. Unfortunately, this does not work with Tomcat 8.5.x. Do you happen to have an update to make this work with Tomcat 8.5.x?Styx
Tomcat 8.5 seems to need "base#socketWrapper#socket#sc#remoteAddress" (notice sos is missing).Purity
Also, this doesn't work if socket is manager by APR (i.e. https).Purity
Tested on Tomcat 9.0.10, it works with "base#socketWrapper#socket#sc#remoteAddress"Shivaree
R
0

If you are using Tyrus which is JSR-356 compliant, then you can get the IP address from the Session instance, but this is a non-standard method.

See here.

Rate answered 4/8, 2014 at 13:14 Comment(0)
B
0

If Springboot with Undertow Websocket engine is used, try below way to get IP.

 @OnOpen
    public void onOpen(Session session) {
        UndertowSession us = (UndertowSession) session;
        String ip = us.getWebSocketChannel().getSourceAddress().getHostString();

Binkley answered 10/8, 2021 at 3:24 Comment(0)
C
0

If using: implementation 'io.quarkus:quarkus-websockets'

@OnOpen
  public void onOpen(final Session session, final @PathParam("userId") String userId) {
    UndertowSession us = (UndertowSession) session;
    System.out.println("Remote Address: " + us.getChannel().remoteAddress());

    SESSIONS.put(userId, session);
    log.info("User " + userId + " joined");
  }
Cynthiacynthie answered 13/6, 2022 at 10:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.