package com.netifera.platform.net.internal.sniffing.stream;
import java.nio.ByteBuffer;
import java.util.Collection;
import com.netifera.platform.api.log.ILogger;
import com.netifera.platform.net.packets.tcpip.TCP;
import com.netifera.platform.net.sniffing.stream.IBlockSnifferHandle;
import com.netifera.platform.net.sniffing.stream.IStreamSniffer;
import com.netifera.platform.net.sniffing.stream.IStreamSnifferHandle;
import com.netifera.platform.net.sniffing.stream.IStreamSniffer.SessionType;
public class TCPSession {
/*
* ======== State Machine Description ========
*
* 1) Triggers which cause state changes
*
* Client to Server:
*
* (syn) : Packet with SYN flag and no ACK flag
* (cdata) : Packet from client to server without SYN flag
* (cfin) : FIN packet from client to server
*
* Server to Client:
*
* (syn,ack) : Packet with both SYN and ACK flags set
* (sdata) : Packet from server to client without SYN flag
* (sfin) : FIN packet from server to client
*
* Either direction:
*
* (rst) : Packet with RST flag
*
*
* 2) State transitions
*
*
* [START]
* / \
* / \
* (syn,ack)> / \ <(syn)
* / \
* / \
* (rst) V V (rst)
* [CLOSED]<------[SYNACK_ONLY] [SYN_RCVD]----------->[CLOSED]
* | | \
* | | \
* (sdata)> | (syn,ack)> | \ <(cdata)
* | | \
* | | \
* V V V
* [ESTABLISHED_SERVER_ONLY] [ESTABLISHED] [ESTABLISHED_CLIENT_ONLY]
* | / | \ |
* | / | \ |
* | (sfin)> / | \ <(cfin) |
* | / | \ |
* | / | \ (cfin)> |
* (sfin)> | / (rst)> | \ or |
* or | V | V (rst)> |
* (rst)> | [SERVER_CLOSED] | [CLIENT_CLOSED] |
* | \ | / |
* | \ | / |
* | (cfin)> \ | / <(sfin) |
* | \ | / |
* | V V V |
* +-------------> [ CLOSED ] <------------------+
*
*/
private enum State {
START, /* Initial state */
SYN_RCVD, /* Initial SYN packet seen */
SYNACK_ONLY, /* SYN,ACK seen with no initial SYN */
ESTABLISHED, /* Active session with both client and server */
ESTABLISHED_CLIENTONLY, /* Established, only client to server traffic visible */
ESTABLISHED_SERVER_ONLY, /* Established, only server to client traffic visible */
CLIENT_CLOSED, /* Client has sent FIN */
SERVER_CLOSED, /* Server has sent FIN */
CLOSED /* Connection closed by RST or FIN(s) */
};
private final TCPSessionKey key;
private TCPAssembler clientToServer;
private TCPAssembler serverToClient;
private final TCPBlockManager blockManager;
private final TCPStreamManager streamManager;
private final TCPReassemblyConfig config;
private final ILogger logger;
private TCP savedSYN;
private State state = State.START;
private long lastActivityTimestamp = 0;
private final Object sessionTag;
public TCPSession(TCPSessionKey key, TCPReassemblyConfig config, ILogger logger, long timestamp, Object tag, Collection<IStreamSnifferHandle> streamHandles,
Collection<IBlockSnifferHandle> blockHandles) {
this.key = key;
this.config = config;
this.logger = logger;
this.lastActivityTimestamp = timestamp;
this.sessionTag = tag;
if(blockHandles != null && blockHandles.size() > 0) {
this.blockManager = new TCPBlockManager(key, sessionTag, blockHandles);
} else {
this.blockManager = null;
}
if(streamHandles != null && streamHandles.size() > 0) {
this.streamManager = new TCPStreamManager(key, sessionTag, streamHandles);
} else {
this.streamManager = null;
}
}
public void unregisterStreamHandle(IStreamSnifferHandle handle) {
logger.debug("Unregister with state = " + state + " for key " + key);
if(streamManager != null) {
streamManager.unregisterHandle(handle);
}
}
public void unregisterBlockHandle(IBlockSnifferHandle handle) {
logger.debug("Unregister (block) with state = " + state + " for key " + key);
if(blockManager != null) {
blockManager.unregisterHandle(handle);
}
}
public boolean isClosedOnTimeout(long currentTimestamp) {
return hasHandshakeTimeoutExpired(currentTimestamp) || hasIdleLimitExpired(currentTimestamp)
|| hasReassemblyTimeoutExpired(currentTimestamp) || hasClosedTimeoutExpired(currentTimestamp);
}
private boolean hasIdleLimitExpired(long currentTimestamp) {
final int timeout = config.getSessionIdleTimeout();
if(timeout == TCPReassemblyConfig.NO_LIMIT) {
return false;
}
if( (currentTimestamp - lastActivityTimestamp) > timeout) {
logger.debug("Idle timeout expired for " + key);
return true;
} else {
return false;
}
}
private boolean hasHandshakeTimeoutExpired(long currentTimestamp) {
final int timeout = config.getSessionHandshakeTimeout();
if(timeout == TCPReassemblyConfig.NO_LIMIT || isHandshakeCompleted())
return false;
else {
if((currentTimestamp - lastActivityTimestamp) > timeout) {
logger.debug("Handshake timeout expired for " + key);
return true;
} else {
return false;
}
}
}
private boolean isHandshakeCompleted() {
return !(state == State.START || state == State.SYN_RCVD || state == State.SYNACK_ONLY);
}
private boolean hasReassemblyTimeoutExpired(long timestamp) {
switch(state) {
case ESTABLISHED:
return checkEstablishedReassemblyExpired(timestamp);
case ESTABLISHED_CLIENTONLY:
return checkOneSidedReassemblyExpired(clientToServer, timestamp);
case ESTABLISHED_SERVER_ONLY:
return checkOneSidedReassemblyExpired(serverToClient, timestamp);
}
return false;
}
private boolean checkEstablishedReassemblyExpired(long timestamp) {
if(clientToServer.hasReassemblyExpired(timestamp)) {
logger.debug("Client side reassembly timed out for " + key);
shutdownSession();
return true;
} else if(serverToClient.hasReassemblyExpired(timestamp)) {
logger.debug("Server side reassembly timed out for " + key);
shutdownSession();
return true;
} else {
return false;
}
}
private boolean checkOneSidedReassemblyExpired(TCPAssembler assembler, long timestamp) {
if(assembler.hasReassemblyExpired(timestamp)) {
logger.debug("Reassembly timed out for " + key);
shutdownSession();
return true;
}
return false;
}
private boolean hasClosedTimeoutExpired(long currentTimeout) {
final int timeout = config.getSessionCloseTimeout();
if(timeout == TCPReassemblyConfig.NO_LIMIT || !isHalfClosed())
return false;
else {
if((currentTimeout - lastActivityTimestamp) > timeout) {
logger.debug("Closed timeout expired for " + key);
return true;
} else {
return false;
}
}
}
private boolean isHalfClosed() {
return state == State.CLIENT_CLOSED || state == State.SERVER_CLOSED;
}
/**
*
* @param tcp TCP segment to add to this session.
* @return false if stream is closed
*/
public boolean addSegment(TCP tcp, long currentTimestamp) {
if(isClosed()) {
return false;
}
if(tcp.getSourcePort() == key.getClientPort()) {
addClientSegment(tcp, currentTimestamp);
} else if(tcp.getSourcePort() == key.getServerPort()) {
addServerSegment(tcp, currentTimestamp);
} else {
throw new IllegalArgumentException("TCP segment does not match this session");
}
if(detectClosed()) {
handleClose();
}
lastActivityTimestamp = currentTimestamp;
return state == State.CLOSED;
}
private void addClientSegment(TCP tcp, long timestamp) {
switch(state) {
case ESTABLISHED:
case ESTABLISHED_CLIENTONLY:
case SERVER_CLOSED:
clientToServer.addSegment(tcp, timestamp);
while(!isClosed() && clientToServer.isDataAvailable()) {
handleClientData();
}
if(!isClosed() && clientToServer.isClosed()) {
if(state == State.ESTABLISHED) {
state = State.CLIENT_CLOSED;
} else {
state = State.CLOSED;
}
}
break;
case SYNACK_ONLY:
case CLIENT_CLOSED:
break;
case ESTABLISHED_SERVER_ONLY:
if((tcp.getFIN() || tcp.getRST()) && config.canProccessPeerFinForHalfSessions()) {
shutdownSession();
}
break;
case START:
if(tcp.getSYN() && !tcp.getACK()) {
state = State.SYN_RCVD;
tcp.persist();
savedSYN = tcp;
}
break;
case SYN_RCVD:
if(tcp.getRST()) {
shutdownSession();
} else if(!tcp.getSYN()) {
clientToServer = new TCPAssembler(savedSYN, config, logger);
savedSYN = null;
state = State.ESTABLISHED_CLIENTONLY;
handleEstablished(SessionType.CLIENT_ONLY);
clientToServer.addSegment(tcp, timestamp);
while(!isClosed() && clientToServer.isDataAvailable()) {
handleClientData();
}
}
break;
default:
throw new IllegalStateException("Unexpected TCPSession state: " + state);
}
}
private void addServerSegment(TCP tcp, long timestamp) {
switch(state) {
case ESTABLISHED:
case ESTABLISHED_SERVER_ONLY:
case CLIENT_CLOSED:
serverToClient.addSegment(tcp, timestamp);
while(!isClosed() && serverToClient.isDataAvailable()) {
handleServerData();
}
if(!isClosed() && serverToClient.isClosed()) {
if(state == State.ESTABLISHED) {
state = State.SERVER_CLOSED;
} else {
state = State.CLOSED;
}
}
break;
case ESTABLISHED_CLIENTONLY:
if((tcp.getFIN() || tcp.getRST()) && config.canProccessPeerFinForHalfSessions()) {
shutdownSession();
}
/* ignore everything from the server now */
break;
case START:
if(tcp.getSYN() && tcp.getACK()) {
state = State.SYNACK_ONLY;
tcp.persist();
savedSYN = tcp;
}
break;
case SYN_RCVD:
if(tcp.getSYN() && tcp.getACK()) {
clientToServer = new TCPAssembler(savedSYN, config, logger);
savedSYN = null;
serverToClient = new TCPAssembler(tcp, config, logger);
state = State.ESTABLISHED;
handleEstablished(SessionType.FULL_SESSION);
} else if(tcp.getRST()) {
state = State.CLOSED;
}
break;
case SYNACK_ONLY:
if(tcp.getRST()) {
state = State.CLOSED;
} else if(!tcp.getSYN()) {
serverToClient = new TCPAssembler(savedSYN, config, logger);
savedSYN = null;
state = State.ESTABLISHED_SERVER_ONLY;
handleEstablished(SessionType.SERVER_ONLY);
serverToClient.addSegment(tcp, timestamp);
while(!isClosed() && serverToClient.isDataAvailable()) {
handleServerData();
}
}
break;
case SERVER_CLOSED:
break;
default:
throw new IllegalStateException("Unexpected TCPSession state: " + state);
}
}
private void handleEstablished(IStreamSniffer.SessionType sessionType) {
if(streamManager != null) {
streamManager.handleEstablished(sessionType);
}
if(!hasActiveHandlers()) {
shutdownSession();
}
}
private void handleClientData() {
final ByteBuffer data = clientToServer.getAvailableData().asReadOnlyBuffer();
if(streamManager != null) {
streamManager.handleClientData(data);
}
if(blockManager != null) {
blockManager.addClientData(data);
}
if(!hasActiveHandlers() || isOverSessionByteLimit()) {
shutdownSession();
}
}
private void handleServerData() {
final ByteBuffer data = serverToClient.getAvailableData().asReadOnlyBuffer();
if(streamManager != null) {
streamManager.handleServerData(data);
}
if(blockManager != null) {
blockManager.addServerData(data);
}
if(!hasActiveHandlers() || isOverSessionByteLimit()) {
if(isOverSessionByteLimit()) {
logger.debug("Exceeded session byte limit for " + key);
}
shutdownSession();
}
}
private boolean isOverSessionByteLimit() {
final long limit = config.getMaximumSessionBytes();
if(limit == TCPReassemblyConfig.NO_LIMIT)
return false;
switch(state) {
case ESTABLISHED:
return clientToServer.getAssembledByteCount() + serverToClient.getAssembledByteCount() >= limit;
case ESTABLISHED_CLIENTONLY:
return clientToServer.getAssembledByteCount() >= limit;
case ESTABLISHED_SERVER_ONLY:
return serverToClient.getAssembledByteCount() >= limit;
default:
return false;
}
}
private void handleClose() {
if(streamManager != null) {
streamManager.handleClose();
}
shutdownSession();
}
private void shutdownSession() {
state = State.CLOSED;
clientToServer = null;
serverToClient = null;
if(blockManager != null) {
blockManager.shutdown();
}
if(streamManager != null) {
streamManager.shutdown();
}
}
private boolean hasActiveHandlers() {
if(blockManager != null && blockManager.isClosed() == false) {
return true;
}
if(streamManager != null && streamManager.isActive()) {
return true;
}
return false;
}
public boolean isClosed() {
return state == State.CLOSED;
}
public TCPSessionKey getSessionKey() {
return key;
}
private boolean detectClosed() {
switch(state) {
case ESTABLISHED:
return (serverToClient.isClosed() && clientToServer.isClosed()) ||
(serverToClient.isReset()) || (clientToServer.isReset());
case ESTABLISHED_CLIENTONLY:
return clientToServer.isClosed();
case ESTABLISHED_SERVER_ONLY:
return serverToClient.isClosed();
}
return false;
}
}