Skip to content

Instantly share code, notes, and snippets.

@adhorn
Forked from ZuZuD/exhaust_ephemeral_ports.py
Created February 21, 2022 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adhorn/aee230f97d7c6b6bfb41f854f1bc3647 to your computer and use it in GitHub Desktop.
Save adhorn/aee230f97d7c6b6bfb41f854f1bc3647 to your computer and use it in GitHub Desktop.
Simulate an ephemeral port exhaustion on a Linux client.
import socket
import time
import argparse
import subprocess
import shlex
"""
Usage: python3 exhaust_ephemeral_ports.py <dst> <dport> <optional:loop>
Example: python3 exhaust_ephemeral_ports.py 172.31.23.144 80
Help: exhaust_ephemeral_ports.py --help
You should have a destionation server (ex: webserver) accepting connections
That won't affect him as opposed to the client we simulate here. Indeed server will
close the connection once it sent FIN (no TIME-WAIT status)
The goal is to see how your system reacts when we exhaust the client ephemeral ports. The syscall connect() should
return EADDRNOTAVAIL (Cannot assign requested address) when no more are available (refere to man 2 connect).
But depending on your system configuration, you might reach a different limit and have a different outcome.
You can run command like this in parallel to help keeping track of the numbers:
- watch -n 0.1 'ss -tlnpa|grep -E ":<dport>"|wc -l'
- watch -n 0.1 'cat /proc/net/nf_conntrack|grep dport=<dport>|wc -l'
Parameters definition:
- ip_local_port_range: Range of local ports for outgoing connections (max 1 to 1 connection for same dst port)
- tcp_max_syn_backlog: Max number of allowed socket in SYN_RECEIVED status (half-open on the receiver side)
- tcp_orphan_retries: How may times to retry before killing TCP connection, closed by our side. Default value 7 corresponds to 50sec-16min depending on RTO
- tcp_max_tw_buckets: Maximal number of timewait sockets held by the system simultaneously. If this number is exceeded time-wait socket is immediately destroyed and a warning is printed. This limit exists only to prevent simple DoS attacks, you must not lower the limit artificially, but rather increase it (probably, after increasing installed memory), if network conditions require more than the default value
- tcp_tw_recycle: Enable fast recycling TIME-WAIT sockets. Default value is 1. It should not be changed without advice/request of technical experts.
- tcp_fin_timeout: Time to hold socket in state FIN-WAIT-2, if it was closed by our side. Peer can be broken and never close its side, or even died unexpectedly. Default value is 60sec. Usual value used in 2.2 was 180 seconds, you may restore it, but remember that if your machine is even underloaded WEB server, you risk to overflow memory with kilotons of dead sockets, FIN-WAIT-2 sockets are less dangerous than FIN-WAIT-1, because they eat maximum 1.5K of memory, but they tend to live longer. Cf. tcp_max_orphans.
- file-max: This is basically the number of file descriptors available in the kernel. Which also affects the number of fd’s a process can have open. For large sites you will definitely need to upgrade this, and for some OS’es you will need to use ulimit to increase the number of fds available for the server process.
"""
def create_timewait_socket(dst, dport):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# s.setblocking(0)
s.connect((dst, dport))
print(f"Connected: {s.getsockname()}")
s.close()
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("dst", help="destination server IP", type=str)
parser.add_argument("dport", help="destination server Port", type=int)
parser.add_argument(
"loop", help="number of loop over the ephemeral port range", nargs='?', type=int, default=5)
args = parser.parse_args()
sleep_time = 10
sys_net_params = ["nf_conntrack_tcp_timeout_fin_wait",
"tcp_max_tw_buckets", "tcp_fin_timeout",
"tcp_orphan_retries", "tcp_max_syn_backlog",
"nf_conntrack_max", "file-max", "pid_max", "ip_local_port_range"]
cmd = shlex.split("find /proc/sys/ -type f")
sysctl_params = [i.decode('utf-8')
for i in subprocess.check_output(cmd).split()]
sysctl_filtered = filter(lambda x: x.split(
'/')[-1] in sys_net_params, sysctl_params)
# display sysctl_filtered values
for param in sysctl_filtered:
# Exception can only occur for ip_local_port_range
# as we need it for create_timewait_socket()
param_name = param.split('/')[-1]
if param_name == "ip_local_port_range":
with open(param) as f:
start_port, end_port = [int(i) for i in f.read().split()]
print(f"{param_name}: {start_port} to {end_port}")
continue
try:
with open(param) as f:
print(f"{param_name}: {''.join(f.read().split())}")
except FileNotFoundError:
print(f"Unable to access {param}")
print(f"\nStarting the exhaustion in {sleep_time}s")
time.sleep(sleep_time)
"""
Now let's loop over the ephemeral range multiple times to exhaust it
loop should be faster than tcp_fin_timeout otherwise exhaustion won't be achievable
tcp_max_tw_buckets can limit the number of TIME-WAIT socket and the related behavior differ:
- Amazon Linux block on connect() once this limit reached
- Ubuntu 18.04 don't block and recycle the connections in TW
nf_conntrack_tcp_timeout_fin_wait usually has a higher value than tcp_fin_timeout.
Anyway conntrack recycle the connection with same ephemeral port in TW status when needed hence
doesn't block
"""
for i in range(args.loop):
for j in range(start_port, end_port):
create_timewait_socket(dst=args.dst, dport=args.dport)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment