mirror of
https://github.com/m1ngsama/TNT.git
synced 2026-03-25 22:33:51 +00:00
fix: separate per-ip concurrency from connection rate
This commit is contained in:
parent
e473b26e0d
commit
cb106de31b
8 changed files with 289 additions and 47 deletions
24
README.md
24
README.md
|
|
@ -106,10 +106,13 @@ TNT_PUBLIC_HOST=chat.m1ng.space tnt
|
||||||
# Max total connections (default 64)
|
# Max total connections (default 64)
|
||||||
TNT_MAX_CONNECTIONS=100 tnt
|
TNT_MAX_CONNECTIONS=100 tnt
|
||||||
|
|
||||||
# Max connections per IP (default 5)
|
# Max concurrent sessions per IP (default 5)
|
||||||
TNT_MAX_CONN_PER_IP=10 tnt
|
TNT_MAX_CONN_PER_IP=10 tnt
|
||||||
|
|
||||||
# Disable rate limiting (testing only)
|
# Max new connection attempts per IP in 60 seconds (default 10)
|
||||||
|
TNT_MAX_CONN_RATE_PER_IP=30 tnt
|
||||||
|
|
||||||
|
# Disable connection-rate and auth-failure blocking (testing only)
|
||||||
TNT_RATE_LIMIT=0 tnt
|
TNT_RATE_LIMIT=0 tnt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -124,11 +127,24 @@ TNT_SSH_LOG_LEVEL=3 tnt
|
||||||
TNT_ACCESS_TOKEN="strong-password-123" \
|
TNT_ACCESS_TOKEN="strong-password-123" \
|
||||||
TNT_BIND_ADDR=0.0.0.0 \
|
TNT_BIND_ADDR=0.0.0.0 \
|
||||||
TNT_MAX_CONNECTIONS=200 \
|
TNT_MAX_CONNECTIONS=200 \
|
||||||
TNT_MAX_CONN_PER_IP=3 \
|
TNT_MAX_CONN_PER_IP=30 \
|
||||||
|
TNT_MAX_CONN_RATE_PER_IP=60 \
|
||||||
TNT_SSH_LOG_LEVEL=1 \
|
TNT_SSH_LOG_LEVEL=1 \
|
||||||
tnt -p 2222
|
tnt -p 2222
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### SSH Exec Interface
|
||||||
|
|
||||||
|
TNT also exposes a small non-interactive SSH surface for scripts:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh -p 2222 chat.m1ng.space health
|
||||||
|
ssh -p 2222 chat.m1ng.space stats --json
|
||||||
|
ssh -p 2222 chat.m1ng.space users
|
||||||
|
ssh -p 2222 chat.m1ng.space "tail -n 20"
|
||||||
|
ssh -p 2222 operator@chat.m1ng.space post "service notice"
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
@ -151,6 +167,7 @@ cd tests
|
||||||
./test_basic.sh # basic functionality
|
./test_basic.sh # basic functionality
|
||||||
./test_security_features.sh # security features
|
./test_security_features.sh # security features
|
||||||
./test_anonymous_access.sh # anonymous access
|
./test_anonymous_access.sh # anonymous access
|
||||||
|
./test_connection_limits.sh # per-IP concurrency and rate limits
|
||||||
./test_stress.sh # stress test
|
./test_stress.sh # stress test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -217,6 +234,7 @@ TNT_BIND_ADDR=0.0.0.0
|
||||||
TNT_STATE_DIR=/var/lib/tnt
|
TNT_STATE_DIR=/var/lib/tnt
|
||||||
TNT_MAX_CONNECTIONS=200
|
TNT_MAX_CONNECTIONS=200
|
||||||
TNT_MAX_CONN_PER_IP=30
|
TNT_MAX_CONN_PER_IP=30
|
||||||
|
TNT_MAX_CONN_RATE_PER_IP=60
|
||||||
TNT_RATE_LIMIT=1
|
TNT_RATE_LIMIT=1
|
||||||
TNT_SSH_LOG_LEVEL=0
|
TNT_SSH_LOG_LEVEL=0
|
||||||
TNT_PUBLIC_HOST=chat.m1ng.space
|
TNT_PUBLIC_HOST=chat.m1ng.space
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ TNT_BIND_ADDR=0.0.0.0
|
||||||
TNT_STATE_DIR=/var/lib/tnt
|
TNT_STATE_DIR=/var/lib/tnt
|
||||||
TNT_MAX_CONNECTIONS=200
|
TNT_MAX_CONNECTIONS=200
|
||||||
TNT_MAX_CONN_PER_IP=30
|
TNT_MAX_CONN_PER_IP=30
|
||||||
|
TNT_MAX_CONN_RATE_PER_IP=60
|
||||||
TNT_RATE_LIMIT=1
|
TNT_RATE_LIMIT=1
|
||||||
TNT_SSH_LOG_LEVEL=0
|
TNT_SSH_LOG_LEVEL=0
|
||||||
TNT_PUBLIC_HOST=chat.m1ng.space
|
TNT_PUBLIC_HOST=chat.m1ng.space
|
||||||
|
|
@ -81,6 +82,13 @@ sudo systemctl restart tnt
|
||||||
The service uses `StateDirectory=tnt`, so systemd creates `/var/lib/tnt` automatically.
|
The service uses `StateDirectory=tnt`, so systemd creates `/var/lib/tnt` automatically.
|
||||||
Use `TNT_STATE_DIR` or `tnt -d DIR` when running outside systemd to avoid depending on the current working directory.
|
Use `TNT_STATE_DIR` or `tnt -d DIR` when running outside systemd to avoid depending on the current working directory.
|
||||||
|
|
||||||
|
Recommended interpretation:
|
||||||
|
|
||||||
|
- `TNT_MAX_CONNECTIONS`: global connection ceiling
|
||||||
|
- `TNT_MAX_CONN_PER_IP`: concurrent sessions allowed from one IP
|
||||||
|
- `TNT_MAX_CONN_RATE_PER_IP`: new connection attempts allowed per IP per 60 seconds
|
||||||
|
- `TNT_RATE_LIMIT=0`: disables rate-based blocking and auth-failure IP blocking, but not the explicit capacity limits
|
||||||
|
|
||||||
## Firewall
|
## Firewall
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ Connect: `sshpass -p "YourSecretPassword" ssh -p 2222 localhost`
|
||||||
| `TNT_SSH_LOG_LEVEL` | `1` | SSH logging (0-4) | `TNT_SSH_LOG_LEVEL=3` |
|
| `TNT_SSH_LOG_LEVEL` | `1` | SSH logging (0-4) | `TNT_SSH_LOG_LEVEL=3` |
|
||||||
| `TNT_RATE_LIMIT` | `1` | Rate limiting on/off | `TNT_RATE_LIMIT=0` |
|
| `TNT_RATE_LIMIT` | `1` | Rate limiting on/off | `TNT_RATE_LIMIT=0` |
|
||||||
| `TNT_MAX_CONNECTIONS` | `64` | Total connection limit | `TNT_MAX_CONNECTIONS=100` |
|
| `TNT_MAX_CONNECTIONS` | `64` | Total connection limit | `TNT_MAX_CONNECTIONS=100` |
|
||||||
| `TNT_MAX_CONN_PER_IP` | `5` | Per-IP limit | `TNT_MAX_CONN_PER_IP=3` |
|
| `TNT_MAX_CONN_PER_IP` | `5` | Concurrent sessions per IP | `TNT_MAX_CONN_PER_IP=3` |
|
||||||
|
| `TNT_MAX_CONN_RATE_PER_IP` | `10` | New connections per IP per 60s | `TNT_MAX_CONN_RATE_PER_IP=20` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -75,7 +76,8 @@ TNT_MAX_CONN_PER_IP=2 \
|
||||||
## Rate Limiting
|
## Rate Limiting
|
||||||
|
|
||||||
### Defaults
|
### Defaults
|
||||||
- **Connection Rate:** 10 connections per IP per 60 seconds
|
- **Concurrent Sessions:** 5 per IP
|
||||||
|
- **Connection Rate:** 10 new connections per IP per 60 seconds
|
||||||
- **Auth Failures:** 5 failures → 5 minute IP block
|
- **Auth Failures:** 5 failures → 5 minute IP block
|
||||||
- **Window:** 60 second rolling window
|
- **Window:** 60 second rolling window
|
||||||
|
|
||||||
|
|
@ -110,6 +112,12 @@ TNT_MAX_CONN_PER_IP=3 ./tnt
|
||||||
```
|
```
|
||||||
Each IP can have max 3 concurrent connections.
|
Each IP can have max 3 concurrent connections.
|
||||||
|
|
||||||
|
### Per-IP Rate Limit
|
||||||
|
```bash
|
||||||
|
TNT_MAX_CONN_RATE_PER_IP=20 ./tnt
|
||||||
|
```
|
||||||
|
Each IP can open at most 20 new connections per 60 seconds before being temporarily blocked.
|
||||||
|
|
||||||
### Combined Example
|
### Combined Example
|
||||||
```bash
|
```bash
|
||||||
TNT_MAX_CONNECTIONS=100 TNT_MAX_CONN_PER_IP=10 ./tnt
|
TNT_MAX_CONNECTIONS=100 TNT_MAX_CONN_PER_IP=10 ./tnt
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "chat_room.h"
|
#include "chat_room.h"
|
||||||
|
#include <arpa/inet.h>
|
||||||
#include <libssh/libssh.h>
|
#include <libssh/libssh.h>
|
||||||
#include <libssh/server.h>
|
#include <libssh/server.h>
|
||||||
|
|
||||||
|
|
@ -12,6 +13,7 @@ typedef struct client {
|
||||||
ssh_session session; /* SSH session */
|
ssh_session session; /* SSH session */
|
||||||
ssh_channel channel; /* SSH channel */
|
ssh_channel channel; /* SSH channel */
|
||||||
char username[MAX_USERNAME_LEN];
|
char username[MAX_USERNAME_LEN];
|
||||||
|
char client_ip[INET6_ADDRSTRLEN];
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
client_mode_t mode;
|
client_mode_t mode;
|
||||||
|
|
|
||||||
149
src/ssh_server.c
149
src/ssh_server.c
|
|
@ -35,6 +35,7 @@ typedef struct {
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
ssh_session session;
|
ssh_session session;
|
||||||
|
char client_ip[INET6_ADDRSTRLEN];
|
||||||
} accepted_session_t;
|
} accepted_session_t;
|
||||||
|
|
||||||
/* Rate limiting and connection tracking */
|
/* Rate limiting and connection tracking */
|
||||||
|
|
@ -46,7 +47,8 @@ typedef struct {
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char ip[INET6_ADDRSTRLEN];
|
char ip[INET6_ADDRSTRLEN];
|
||||||
time_t window_start;
|
time_t window_start;
|
||||||
int connection_count;
|
int recent_connection_count;
|
||||||
|
int active_connections;
|
||||||
int auth_failure_count;
|
int auth_failure_count;
|
||||||
bool is_blocked;
|
bool is_blocked;
|
||||||
time_t block_until;
|
time_t block_until;
|
||||||
|
|
@ -61,6 +63,7 @@ static time_t g_server_start_time = 0;
|
||||||
/* Configuration from environment variables */
|
/* Configuration from environment variables */
|
||||||
static int g_max_connections = 64;
|
static int g_max_connections = 64;
|
||||||
static int g_max_conn_per_ip = 5;
|
static int g_max_conn_per_ip = 5;
|
||||||
|
static int g_max_conn_rate_per_ip = 10;
|
||||||
static int g_rate_limit_enabled = 1;
|
static int g_rate_limit_enabled = 1;
|
||||||
static char g_access_token[256] = "";
|
static char g_access_token[256] = "";
|
||||||
|
|
||||||
|
|
@ -118,11 +121,18 @@ static void init_rate_limit_config(void) {
|
||||||
|
|
||||||
if ((env = getenv("TNT_MAX_CONN_PER_IP")) != NULL) {
|
if ((env = getenv("TNT_MAX_CONN_PER_IP")) != NULL) {
|
||||||
int val = atoi(env);
|
int val = atoi(env);
|
||||||
if (val > 0 && val <= 100) {
|
if (val > 0 && val <= 1024) {
|
||||||
g_max_conn_per_ip = val;
|
g_max_conn_per_ip = val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((env = getenv("TNT_MAX_CONN_RATE_PER_IP")) != NULL) {
|
||||||
|
int val = atoi(env);
|
||||||
|
if (val > 0 && val <= 1024) {
|
||||||
|
g_max_conn_rate_per_ip = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((env = getenv("TNT_RATE_LIMIT")) != NULL) {
|
if ((env = getenv("TNT_RATE_LIMIT")) != NULL) {
|
||||||
g_rate_limit_enabled = atoi(env);
|
g_rate_limit_enabled = atoi(env);
|
||||||
}
|
}
|
||||||
|
|
@ -147,7 +157,8 @@ static ip_rate_limit_t* get_rate_limit_entry(const char *ip) {
|
||||||
if (g_rate_limits[i].ip[0] == '\0') {
|
if (g_rate_limits[i].ip[0] == '\0') {
|
||||||
strncpy(g_rate_limits[i].ip, ip, sizeof(g_rate_limits[i].ip) - 1);
|
strncpy(g_rate_limits[i].ip, ip, sizeof(g_rate_limits[i].ip) - 1);
|
||||||
g_rate_limits[i].window_start = time(NULL);
|
g_rate_limits[i].window_start = time(NULL);
|
||||||
g_rate_limits[i].connection_count = 0;
|
g_rate_limits[i].recent_connection_count = 0;
|
||||||
|
g_rate_limits[i].active_connections = 0;
|
||||||
g_rate_limits[i].auth_failure_count = 0;
|
g_rate_limits[i].auth_failure_count = 0;
|
||||||
g_rate_limits[i].is_blocked = false;
|
g_rate_limits[i].is_blocked = false;
|
||||||
g_rate_limits[i].block_until = 0;
|
g_rate_limits[i].block_until = 0;
|
||||||
|
|
@ -155,67 +166,83 @@ static ip_rate_limit_t* get_rate_limit_entry(const char *ip) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Find oldest entry to replace */
|
/* Reuse the oldest inactive entry first so active IP accounting stays intact. */
|
||||||
int oldest_idx = 0;
|
int oldest_idx = -1;
|
||||||
time_t oldest_time = g_rate_limits[0].window_start;
|
time_t oldest_time = 0;
|
||||||
for (int i = 1; i < MAX_TRACKED_IPS; i++) {
|
for (int i = 0; i < MAX_TRACKED_IPS; i++) {
|
||||||
if (g_rate_limits[i].window_start < oldest_time) {
|
if (g_rate_limits[i].active_connections != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (oldest_idx < 0 || g_rate_limits[i].window_start < oldest_time) {
|
||||||
oldest_time = g_rate_limits[i].window_start;
|
oldest_time = g_rate_limits[i].window_start;
|
||||||
oldest_idx = i;
|
oldest_idx = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldest_idx < 0) {
|
||||||
|
oldest_idx = 0;
|
||||||
|
oldest_time = g_rate_limits[0].window_start;
|
||||||
|
for (int i = 1; i < MAX_TRACKED_IPS; i++) {
|
||||||
|
if (g_rate_limits[i].window_start < oldest_time) {
|
||||||
|
oldest_time = g_rate_limits[i].window_start;
|
||||||
|
oldest_idx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Reset and reuse */
|
/* Reset and reuse */
|
||||||
strncpy(g_rate_limits[oldest_idx].ip, ip, sizeof(g_rate_limits[oldest_idx].ip) - 1);
|
strncpy(g_rate_limits[oldest_idx].ip, ip, sizeof(g_rate_limits[oldest_idx].ip) - 1);
|
||||||
g_rate_limits[oldest_idx].ip[sizeof(g_rate_limits[oldest_idx].ip) - 1] = '\0';
|
g_rate_limits[oldest_idx].ip[sizeof(g_rate_limits[oldest_idx].ip) - 1] = '\0';
|
||||||
g_rate_limits[oldest_idx].window_start = time(NULL);
|
g_rate_limits[oldest_idx].window_start = time(NULL);
|
||||||
g_rate_limits[oldest_idx].connection_count = 0;
|
g_rate_limits[oldest_idx].recent_connection_count = 0;
|
||||||
|
g_rate_limits[oldest_idx].active_connections = 0;
|
||||||
g_rate_limits[oldest_idx].auth_failure_count = 0;
|
g_rate_limits[oldest_idx].auth_failure_count = 0;
|
||||||
g_rate_limits[oldest_idx].is_blocked = false;
|
g_rate_limits[oldest_idx].is_blocked = false;
|
||||||
g_rate_limits[oldest_idx].block_until = 0;
|
g_rate_limits[oldest_idx].block_until = 0;
|
||||||
return &g_rate_limits[oldest_idx];
|
return &g_rate_limits[oldest_idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check rate limit for an IP */
|
/* Check rate and concurrency limits for an IP */
|
||||||
static bool check_rate_limit(const char *ip) {
|
static bool check_ip_connection_policy(const char *ip) {
|
||||||
if (!g_rate_limit_enabled) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
time_t now = time(NULL);
|
time_t now = time(NULL);
|
||||||
|
|
||||||
pthread_mutex_lock(&g_rate_limit_lock);
|
pthread_mutex_lock(&g_rate_limit_lock);
|
||||||
ip_rate_limit_t *entry = get_rate_limit_entry(ip);
|
ip_rate_limit_t *entry = get_rate_limit_entry(ip);
|
||||||
|
|
||||||
/* Check if blocked */
|
if (entry->active_connections >= g_max_conn_per_ip) {
|
||||||
if (entry->is_blocked && now < entry->block_until) {
|
pthread_mutex_unlock(&g_rate_limit_lock);
|
||||||
|
fprintf(stderr, "Concurrent IP limit reached for %s\n", ip);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_rate_limit_enabled && entry->is_blocked && now < entry->block_until) {
|
||||||
pthread_mutex_unlock(&g_rate_limit_lock);
|
pthread_mutex_unlock(&g_rate_limit_lock);
|
||||||
fprintf(stderr, "Blocked IP %s (blocked until %ld)\n", ip, (long)entry->block_until);
|
fprintf(stderr, "Blocked IP %s (blocked until %ld)\n", ip, (long)entry->block_until);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Unblock if block duration passed */
|
if (g_rate_limit_enabled && entry->is_blocked && now >= entry->block_until) {
|
||||||
if (entry->is_blocked && now >= entry->block_until) {
|
|
||||||
entry->is_blocked = false;
|
entry->is_blocked = false;
|
||||||
entry->auth_failure_count = 0;
|
entry->auth_failure_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reset window if expired */
|
if (g_rate_limit_enabled) {
|
||||||
if (now - entry->window_start >= RATE_LIMIT_WINDOW) {
|
if (now - entry->window_start >= RATE_LIMIT_WINDOW) {
|
||||||
entry->window_start = now;
|
entry->window_start = now;
|
||||||
entry->connection_count = 0;
|
entry->recent_connection_count = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check connection rate */
|
entry->recent_connection_count++;
|
||||||
entry->connection_count++;
|
if (entry->recent_connection_count > g_max_conn_rate_per_ip) {
|
||||||
if (entry->connection_count > g_max_conn_per_ip) {
|
entry->is_blocked = true;
|
||||||
entry->is_blocked = true;
|
entry->block_until = now + BLOCK_DURATION;
|
||||||
entry->block_until = now + BLOCK_DURATION;
|
pthread_mutex_unlock(&g_rate_limit_lock);
|
||||||
pthread_mutex_unlock(&g_rate_limit_lock);
|
fprintf(stderr, "Rate limit exceeded for IP %s\n", ip);
|
||||||
fprintf(stderr, "Rate limit exceeded for IP %s\n", ip);
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry->active_connections++;
|
||||||
pthread_mutex_unlock(&g_rate_limit_lock);
|
pthread_mutex_unlock(&g_rate_limit_lock);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -224,6 +251,10 @@ static bool check_rate_limit(const char *ip) {
|
||||||
static void record_auth_failure(const char *ip) {
|
static void record_auth_failure(const char *ip) {
|
||||||
time_t now = time(NULL);
|
time_t now = time(NULL);
|
||||||
|
|
||||||
|
if (!g_rate_limit_enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
pthread_mutex_lock(&g_rate_limit_lock);
|
pthread_mutex_lock(&g_rate_limit_lock);
|
||||||
ip_rate_limit_t *entry = get_rate_limit_entry(ip);
|
ip_rate_limit_t *entry = get_rate_limit_entry(ip);
|
||||||
|
|
||||||
|
|
@ -237,6 +268,19 @@ static void record_auth_failure(const char *ip) {
|
||||||
pthread_mutex_unlock(&g_rate_limit_lock);
|
pthread_mutex_unlock(&g_rate_limit_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void release_ip_connection(const char *ip) {
|
||||||
|
if (!ip || ip[0] == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_rate_limit_lock);
|
||||||
|
ip_rate_limit_t *entry = get_rate_limit_entry(ip);
|
||||||
|
if (entry->active_connections > 0) {
|
||||||
|
entry->active_connections--;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&g_rate_limit_lock);
|
||||||
|
}
|
||||||
|
|
||||||
/* Check and increment total connection count */
|
/* Check and increment total connection count */
|
||||||
static bool check_and_increment_connections(void) {
|
static bool check_and_increment_connections(void) {
|
||||||
pthread_mutex_lock(&g_conn_count_lock);
|
pthread_mutex_lock(&g_conn_count_lock);
|
||||||
|
|
@ -1447,6 +1491,8 @@ cleanup:
|
||||||
room_broadcast(g_room, &leave_msg);
|
room_broadcast(g_room, &leave_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
release_ip_connection(client->client_ip);
|
||||||
|
|
||||||
/* Release the main reference - client will be freed when all refs are gone */
|
/* Release the main reference - client will be freed when all refs are gone */
|
||||||
client_release(client);
|
client_release(client);
|
||||||
|
|
||||||
|
|
@ -1566,6 +1612,9 @@ static void cleanup_failed_session(ssh_session session, session_context_t *ctx)
|
||||||
ssh_free(session);
|
ssh_free(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ctx) {
|
||||||
|
release_ip_connection(ctx->client_ip);
|
||||||
|
}
|
||||||
destroy_session_context(ctx);
|
destroy_session_context(ctx);
|
||||||
decrement_connections();
|
decrement_connections();
|
||||||
}
|
}
|
||||||
|
|
@ -1768,23 +1817,32 @@ static void *bootstrap_client_session(void *arg) {
|
||||||
client_t *client = NULL;
|
client_t *client = NULL;
|
||||||
bool timed_out = false;
|
bool timed_out = false;
|
||||||
time_t start_time;
|
time_t start_time;
|
||||||
|
char accepted_ip[INET6_ADDRSTRLEN] = "";
|
||||||
|
|
||||||
if (!accepted) {
|
if (!accepted) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
session = accepted->session;
|
session = accepted->session;
|
||||||
|
if (accepted->client_ip[0] != '\0') {
|
||||||
|
snprintf(accepted_ip, sizeof(accepted_ip), "%s", accepted->client_ip);
|
||||||
|
}
|
||||||
free(accepted);
|
free(accepted);
|
||||||
|
|
||||||
ctx = calloc(1, sizeof(session_context_t));
|
ctx = calloc(1, sizeof(session_context_t));
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
|
release_ip_connection(accepted_ip);
|
||||||
ssh_disconnect(session);
|
ssh_disconnect(session);
|
||||||
ssh_free(session);
|
ssh_free(session);
|
||||||
decrement_connections();
|
decrement_connections();
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_client_ip(session, ctx->client_ip, sizeof(ctx->client_ip));
|
if (accepted_ip[0] != '\0') {
|
||||||
|
snprintf(ctx->client_ip, sizeof(ctx->client_ip), "%s", accepted_ip);
|
||||||
|
} else {
|
||||||
|
get_client_ip(session, ctx->client_ip, sizeof(ctx->client_ip));
|
||||||
|
}
|
||||||
ctx->pty_width = 80;
|
ctx->pty_width = 80;
|
||||||
ctx->pty_height = 24;
|
ctx->pty_height = 24;
|
||||||
ctx->exec_command[0] = '\0';
|
ctx->exec_command[0] = '\0';
|
||||||
|
|
@ -1881,6 +1939,10 @@ static void *bootstrap_client_session(void *arg) {
|
||||||
sizeof(client->ssh_login) - 1);
|
sizeof(client->ssh_login) - 1);
|
||||||
client->ssh_login[sizeof(client->ssh_login) - 1] = '\0';
|
client->ssh_login[sizeof(client->ssh_login) - 1] = '\0';
|
||||||
}
|
}
|
||||||
|
if (ctx->client_ip[0] != '\0') {
|
||||||
|
snprintf(client->client_ip, sizeof(client->client_ip), "%s",
|
||||||
|
ctx->client_ip);
|
||||||
|
}
|
||||||
if (ctx->exec_command[0] != '\0') {
|
if (ctx->exec_command[0] != '\0') {
|
||||||
strncpy(client->exec_command, ctx->exec_command,
|
strncpy(client->exec_command, ctx->exec_command,
|
||||||
sizeof(client->exec_command) - 1);
|
sizeof(client->exec_command) - 1);
|
||||||
|
|
@ -1991,13 +2053,6 @@ int ssh_server_start(int unused) {
|
||||||
|
|
||||||
get_client_ip(session, client_ip, sizeof(client_ip));
|
get_client_ip(session, client_ip, sizeof(client_ip));
|
||||||
|
|
||||||
/* Check rate limit */
|
|
||||||
if (!check_rate_limit(client_ip)) {
|
|
||||||
ssh_disconnect(session);
|
|
||||||
ssh_free(session);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Check total connection limit */
|
/* Check total connection limit */
|
||||||
if (!check_and_increment_connections()) {
|
if (!check_and_increment_connections()) {
|
||||||
fprintf(stderr, "Max connections reached, rejecting %s\n", client_ip);
|
fprintf(stderr, "Max connections reached, rejecting %s\n", client_ip);
|
||||||
|
|
@ -2005,8 +2060,17 @@ int ssh_server_start(int unused) {
|
||||||
ssh_free(session);
|
ssh_free(session);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!check_ip_connection_policy(client_ip)) {
|
||||||
|
decrement_connections();
|
||||||
|
ssh_disconnect(session);
|
||||||
|
ssh_free(session);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
accepted = calloc(1, sizeof(*accepted));
|
accepted = calloc(1, sizeof(*accepted));
|
||||||
if (!accepted) {
|
if (!accepted) {
|
||||||
|
release_ip_connection(client_ip);
|
||||||
decrement_connections();
|
decrement_connections();
|
||||||
ssh_disconnect(session);
|
ssh_disconnect(session);
|
||||||
ssh_free(session);
|
ssh_free(session);
|
||||||
|
|
@ -2014,10 +2078,13 @@ int ssh_server_start(int unused) {
|
||||||
}
|
}
|
||||||
|
|
||||||
accepted->session = session;
|
accepted->session = session;
|
||||||
|
snprintf(accepted->client_ip, sizeof(accepted->client_ip), "%s",
|
||||||
|
client_ip);
|
||||||
|
|
||||||
if (pthread_create(&thread, &attr, bootstrap_client_session, accepted) != 0) {
|
if (pthread_create(&thread, &attr, bootstrap_client_session, accepted) != 0) {
|
||||||
fprintf(stderr, "Thread creation failed: %s\n", strerror(errno));
|
fprintf(stderr, "Thread creation failed: %s\n", strerror(errno));
|
||||||
free(accepted);
|
free(accepted);
|
||||||
|
release_ip_connection(client_ip);
|
||||||
decrement_connections();
|
decrement_connections();
|
||||||
ssh_disconnect(session);
|
ssh_disconnect(session);
|
||||||
ssh_free(session);
|
ssh_free(session);
|
||||||
|
|
|
||||||
135
tests/test_connection_limits.sh
Executable file
135
tests/test_connection_limits.sh
Executable file
|
|
@ -0,0 +1,135 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Connection limit regression tests for TNT
|
||||||
|
|
||||||
|
PORT=${PORT:-2222}
|
||||||
|
BIN="../tnt"
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
STATE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tnt-limit-test.XXXXXX")
|
||||||
|
SERVER_PID=""
|
||||||
|
WATCHER_PID=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "$WATCHER_PID" ]; then
|
||||||
|
kill "$WATCHER_PID" 2>/dev/null || true
|
||||||
|
wait "$WATCHER_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [ -n "$SERVER_PID" ]; then
|
||||||
|
kill "$SERVER_PID" 2>/dev/null || true
|
||||||
|
wait "$SERVER_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -rf "$STATE_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if [ ! -f "$BIN" ]; then
|
||||||
|
echo "Error: Binary $BIN not found. Run make first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -p $PORT"
|
||||||
|
|
||||||
|
wait_for_health() {
|
||||||
|
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
|
||||||
|
if [ -n "$SERVER_PID" ] && ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
OUT=$(ssh $SSH_OPTS localhost health 2>/dev/null || true)
|
||||||
|
[ "$OUT" = "ok" ] && return 0
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== TNT Connection Limit Tests ==="
|
||||||
|
|
||||||
|
TNT_RATE_LIMIT=0 TNT_MAX_CONN_PER_IP=1 "$BIN" -p "$PORT" -d "$STATE_DIR" \
|
||||||
|
>"$STATE_DIR/concurrent.log" 2>&1 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
if wait_for_health; then
|
||||||
|
echo "✓ server started for concurrent limit test"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo "✗ server failed to start for concurrent limit test"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
WATCHER_READY="$STATE_DIR/watcher.ready"
|
||||||
|
cat >"$STATE_DIR/watcher.expect" <<EOF
|
||||||
|
set timeout 10
|
||||||
|
spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $PORT watcher@localhost
|
||||||
|
expect "请输入用户名"
|
||||||
|
send "watcher\r"
|
||||||
|
exec touch "$WATCHER_READY"
|
||||||
|
sleep 8
|
||||||
|
send "\003"
|
||||||
|
expect eof
|
||||||
|
EOF
|
||||||
|
|
||||||
|
expect "$STATE_DIR/watcher.expect" >"$STATE_DIR/watcher.log" 2>&1 &
|
||||||
|
WATCHER_PID=$!
|
||||||
|
|
||||||
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
[ -f "$WATCHER_READY" ] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ! -f "$WATCHER_READY" ]; then
|
||||||
|
echo "✗ watcher session did not become ready"
|
||||||
|
sed -n '1,120p' "$STATE_DIR/watcher.log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ssh $SSH_OPTS localhost health >/dev/null 2>&1; then
|
||||||
|
echo "✗ concurrent per-IP limit was not enforced"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
else
|
||||||
|
echo "✓ concurrent per-IP limit rejects a second session"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
wait "$WATCHER_PID" 2>/dev/null || true
|
||||||
|
WATCHER_PID=""
|
||||||
|
kill "$SERVER_PID" 2>/dev/null || true
|
||||||
|
wait "$SERVER_PID" 2>/dev/null || true
|
||||||
|
SERVER_PID=""
|
||||||
|
|
||||||
|
RATE_PORT=$((PORT + 1))
|
||||||
|
SSH_RATE_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -p $RATE_PORT"
|
||||||
|
|
||||||
|
TNT_MAX_CONN_PER_IP=10 TNT_MAX_CONN_RATE_PER_IP=2 "$BIN" -p "$RATE_PORT" -d "$STATE_DIR" \
|
||||||
|
>"$STATE_DIR/rate.log" 2>&1 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
if kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
|
echo "✓ server started for rate limit test"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo "✗ server failed to start for rate limit test"
|
||||||
|
sed -n '1,120p' "$STATE_DIR/rate.log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
R1=$(ssh $SSH_RATE_OPTS localhost health 2>/dev/null || true)
|
||||||
|
R2=$(ssh $SSH_RATE_OPTS localhost health 2>/dev/null || true)
|
||||||
|
if ssh $SSH_RATE_OPTS localhost health >/dev/null 2>&1; then
|
||||||
|
echo "✗ per-IP connection-rate limit was not enforced"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
else
|
||||||
|
if [ "$R1" = "ok" ] && [ "$R2" = "ok" ]; then
|
||||||
|
echo "✓ per-IP connection-rate limit blocks after the configured burst"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo "✗ per-IP connection-rate limit setup failed unexpectedly"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "PASSED: $PASS"
|
||||||
|
echo "FAILED: $FAIL"
|
||||||
|
[ "$FAIL" -eq 0 ] && echo "All tests passed" || echo "Some tests failed"
|
||||||
|
exit "$FAIL"
|
||||||
|
|
@ -97,6 +97,10 @@ TNT_ACCESS_TOKEN="test123" $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server"
|
||||||
TNT_MAX_CONNECTIONS=10 $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \
|
TNT_MAX_CONNECTIONS=10 $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \
|
||||||
pass "TNT_MAX_CONNECTIONS configuration accepted" || fail "TNT_MAX_CONNECTIONS not working"
|
pass "TNT_MAX_CONNECTIONS configuration accepted" || fail "TNT_MAX_CONNECTIONS not working"
|
||||||
|
|
||||||
|
# Test per-IP connection rate configuration
|
||||||
|
TNT_MAX_CONN_RATE_PER_IP=20 $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \
|
||||||
|
pass "TNT_MAX_CONN_RATE_PER_IP configuration accepted" || fail "TNT_MAX_CONN_RATE_PER_IP not working"
|
||||||
|
|
||||||
# Test rate limit toggle
|
# Test rate limit toggle
|
||||||
TNT_RATE_LIMIT=0 $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \
|
TNT_RATE_LIMIT=0 $TIMEOUT_CMD 3 $BIN 2>&1 | grep -q "TNT chat server" && \
|
||||||
pass "TNT_RATE_LIMIT configuration accepted" || fail "TNT_RATE_LIMIT not working"
|
pass "TNT_RATE_LIMIT configuration accepted" || fail "TNT_RATE_LIMIT not working"
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ if command -v gtimeout >/dev/null 2>&1; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting TNT server on port $PORT..."
|
echo "Starting TNT server on port $PORT..."
|
||||||
TNT_RATE_LIMIT=0 $BIN -p $PORT &
|
TNT_RATE_LIMIT=0 TNT_MAX_CONN_PER_IP=$CLIENTS $BIN -p $PORT &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue