Skip to content

Commit

Permalink
Separate UDP & TCP session keys to fix VPN bug
Browse files Browse the repository at this point in the history
Previously, sessions were tracked in one big session table, keyed by
source/destionation ip/port pairs. This sounds great, until you release
that actually UDP & TCP ports are independent, and so it's totally
possible to create a new TCP connection on the same ports that are being
used for an existing UDP connection. Uh oh.

In practice, this meant that SYN packets for TCP connections on existing
UDP ports would attempt to re-ack, rejecting the SYN as a duplicate for
an existing TCP connection. This would then fail, because the session
didn't actually have existing TCP data required to do this. Oops.

We now track them separately, which should avoid that situation
entirely.
  • Loading branch information
pimterry committed May 19, 2023
1 parent 6919196 commit de8b3c7
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 26 deletions.
18 changes: 14 additions & 4 deletions app/src/main/java/tech/httptoolkit/android/vpn/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public class Session {
private final String TAG = "Session";

private AbstractSelectableChannel channel;

private final SessionProtocol protocol;

private final int destIp;
private final int destPort;
Expand Down Expand Up @@ -107,6 +109,7 @@ public class Session {
private final ICloseSession sessionCloser;

Session(
SessionProtocol protocol,
int sourceIp,
int sourcePort,
int destinationIp,
Expand All @@ -116,6 +119,7 @@ public class Session {
receivingStream = new ByteArrayOutputStream();
sendingStream = new ByteArrayOutputStream();

this.protocol = protocol;
this.sourceIp = sourceIp;
this.sourcePort = sourcePort;
this.destIp = destinationIp;
Expand Down Expand Up @@ -193,6 +197,10 @@ public boolean hasDataToSend(){
return sendingStream.size() > 0;
}

public SessionProtocol getProtocol() {
return this.protocol;
}

public int getDestIp() {
return destIp;
}
Expand Down Expand Up @@ -363,12 +371,14 @@ public void closeSession() {
}

public String getSessionKey() {
return Session.getSessionKey(this.destIp, this.destPort, this.sourceIp, this.sourcePort);
return Session.getSessionKey(this.protocol, this.destIp, this.destPort, this.sourceIp, this.sourcePort);
}

public static String getSessionKey(int destIp, int destPort, int sourceIp, int sourcePort) {
return PacketUtil.intToIPAddress(sourceIp) + ":" + sourcePort + "->" +
PacketUtil.intToIPAddress(destIp) + ":" + destPort;
public static String getSessionKey(SessionProtocol protocol, int destIp, int destPort, int sourceIp, int sourcePort) {
return protocol.name() + "|" +
PacketUtil.intToIPAddress(sourceIp) + ":" + sourcePort +
"->" +
PacketUtil.intToIPAddress(destIp) + ":" + destPort;
}

public String toString() {
Expand Down
20 changes: 12 additions & 8 deletions app/src/main/java/tech/httptoolkit/android/vpn/SessionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,11 @@ private void handleUDPPacket(ByteBuffer clientPacketData, IPv4Header ipHeader) t
UDPHeader udpheader = UDPPacketFactory.createUDPHeader(clientPacketData);

Session session = manager.getSession(
ipHeader.getDestinationIP(), udpheader.getDestinationPort(),
ipHeader.getSourceIP(), udpheader.getSourcePort()
SessionProtocol.UDP,
ipHeader.getDestinationIP(),
udpheader.getDestinationPort(),
ipHeader.getSourceIP(),
udpheader.getSourcePort()
);

boolean newSession = session == null;
Expand Down Expand Up @@ -140,7 +143,7 @@ private void handleTCPPacket(ByteBuffer clientPacketData, IPv4Header ipHeader) t
// 3-way handshake + create new session
replySynAck(ipHeader,tcpheader);
} else if(tcpheader.isACK()) {
String key = Session.getSessionKey(destinationIP, destinationPort, sourceIP, sourcePort);
String key = Session.getSessionKey(SessionProtocol.TCP, destinationIP, destinationPort, sourceIP, sourcePort);
Session session = manager.getSessionByKey(key);

if (session == null) {
Expand Down Expand Up @@ -176,7 +179,7 @@ private void handleTCPPacket(ByteBuffer clientPacketData, IPv4Header ipHeader) t
sendFinAck(ipHeader, tcpheader, session);
} else if (session.isAckedToFin() && !tcpheader.isFIN()) {
//the last ACK from client after FIN-ACK flag was sent
manager.closeSession(destinationIP, destinationPort, sourceIP, sourcePort);
manager.closeSession(SessionProtocol.TCP, destinationIP, destinationPort, sourceIP, sourcePort);
Log.d(TAG, "got last ACK after FIN, session is now closed.");
}
}
Expand All @@ -190,7 +193,7 @@ private void handleTCPPacket(ByteBuffer clientPacketData, IPv4Header ipHeader) t
Log.d(TAG, "FIN from vpn client, will ack it.");
ackFinAck(ipHeader, tcpheader, session);
} else if (tcpheader.isRST()) {
resetConnection(ipHeader, tcpheader);
resetTCPConnection(ipHeader, tcpheader);
}

if (!session.isAbortingConnection()) {
Expand All @@ -199,14 +202,14 @@ private void handleTCPPacket(ByteBuffer clientPacketData, IPv4Header ipHeader) t
}
} else if(tcpheader.isFIN()){
//case client sent FIN without ACK
Session session = manager.getSession(destinationIP, destinationPort, sourceIP, sourcePort);
Session session = manager.getSession(SessionProtocol.TCP, destinationIP, destinationPort, sourceIP, sourcePort);
if(session == null)
ackFinAck(ipHeader, tcpheader, null);
else
manager.keepSessionAlive(session);

} else if(tcpheader.isRST()){
resetConnection(ipHeader, tcpheader);
resetTCPConnection(ipHeader, tcpheader);
} else {
Log.d(TAG,"unknown TCP flag");
String str1 = PacketUtil.getOutput(ipHeader, tcpheader, clientPacketData.array());
Expand Down Expand Up @@ -364,8 +367,9 @@ private void acceptAck(TCPHeader tcpHeader, Session session){
* @param ip IP
* @param tcp TCP
*/
private void resetConnection(IPv4Header ip, TCPHeader tcp){
private void resetTCPConnection(IPv4Header ip, TCPHeader tcp){
Session session = manager.getSession(
SessionProtocol.TCP,
ip.getDestinationIP(), tcp.getDestinationPort(),
ip.getSourceIP(), tcp.getSourcePort()
);
Expand Down
30 changes: 16 additions & 14 deletions app/src/main/java/tech/httptoolkit/android/vpn/SessionManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,13 @@
import tech.httptoolkit.android.TagKt;
import tech.httptoolkit.android.vpn.socket.DataConst;
import tech.httptoolkit.android.vpn.socket.ICloseSession;
import tech.httptoolkit.android.vpn.socket.SocketNIODataService;
import tech.httptoolkit.android.vpn.socket.SocketProtector;
import tech.httptoolkit.android.vpn.util.PacketUtil;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.AbstractSelectableChannel;
Expand All @@ -59,8 +56,13 @@ public class SessionManager implements ICloseSession {
*/
public void keepSessionAlive(Session session) {
if(session != null){
String key = Session.getSessionKey(session.getDestIp(), session.getDestPort(),
session.getSourceIp(), session.getSourcePort());
String key = Session.getSessionKey(
session.getProtocol(),
session.getDestIp(),
session.getDestPort(),
session.getSourceIp(),
session.getSourcePort()
);
table.put(key, session);
}
}
Expand All @@ -77,8 +79,8 @@ public int addClientData(ByteBuffer buffer, Session session) {
return session.setSendingData(buffer);
}

public Session getSession(int ip, int port, int srcIp, int srcPort) {
String key = Session.getSessionKey(ip, port, srcIp, srcPort);
public Session getSession(SessionProtocol protocol, int ip, int port, int srcIp, int srcPort) {
String key = Session.getSessionKey(protocol, ip, port, srcIp, srcPort);

return getSessionByKey(key);
}
Expand All @@ -99,8 +101,8 @@ public Session getSessionByKey(String key) {
* @param srcIp Source IP Address
* @param srcPort Source Port
*/
public void closeSession(int ip, int port, int srcIp, int srcPort){
String key = Session.getSessionKey(ip, port, srcIp, srcPort);
public void closeSession(SessionProtocol protocol, int ip, int port, int srcIp, int srcPort){
String key = Session.getSessionKey(protocol, ip, port, srcIp, srcPort);
Session session = table.remove(key);

if(session != null){
Expand All @@ -117,21 +119,21 @@ public void closeSession(int ip, int port, int srcIp, int srcPort){
}

public void closeSession(@NonNull Session session){
closeSession(session.getDestIp(),
closeSession(session.getProtocol(), session.getDestIp(),
session.getDestPort(), session.getSourceIp(),
session.getSourcePort());
}

@NotNull
public Session createNewUDPSession(int ip, int port, int srcIp, int srcPort) throws IOException {
String keys = Session.getSessionKey(ip, port, srcIp, srcPort);
String keys = Session.getSessionKey(SessionProtocol.UDP, ip, port, srcIp, srcPort);

// For TCP, we freak out if you try to create an already existing session.
// With UDP though, it's totally fine:
Session existingSession = table.get(keys);
if (existingSession != null) return existingSession;

Session session = new Session(srcIp, srcPort, ip, port, this);
Session session = new Session(SessionProtocol.UDP, srcIp, srcPort, ip, port, this);

DatagramChannel channel;

Expand Down Expand Up @@ -160,7 +162,7 @@ public Session createNewUDPSession(int ip, int port, int srcIp, int srcPort) thr

@NotNull
public Session createNewTCPSession(int ip, int port, int srcIp, int srcPort) throws IOException {
String key = Session.getSessionKey(ip, port, srcIp, srcPort);
String key = Session.getSessionKey(SessionProtocol.TCP, ip, port, srcIp, srcPort);

Session existingSession = table.get(key);

Expand All @@ -169,7 +171,7 @@ public Session createNewTCPSession(int ip, int port, int srcIp, int srcPort) thr
// We return the initialized session, which will be reacked to indicate rejection.
if (existingSession != null) return existingSession;

Session session = new Session(srcIp, srcPort, ip, port, this);
Session session = new Session(SessionProtocol.TCP, srcIp, srcPort, ip, port, this);

SocketChannel channel;
channel = SocketChannel.open();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package tech.httptoolkit.android.vpn;

public enum SessionProtocol {
TCP,
UDP
}

0 comments on commit de8b3c7

Please sign in to comment.