Replace telnet with SSH and fix full-screen display

- Implement SSH server using libssh for secure connections
- Replace insecure telnet with encrypted SSH protocol
- Add automatic terminal size detection via PTY requests
- Support dynamic window resize (SIGWINCH handling)
- Fix UI display bug by using SSH channel instead of fd
- Update tui_clear_screen to work with SSH connections
- Add RSA host key auto-generation on first run
- Update README with SSH instructions and security notes
- Add libssh dependency to Makefile with auto-detection
- Remove all telnet-related code

Security improvements:
- All traffic now encrypted
- Host key authentication
- No more plaintext transmission
This commit is contained in:
m1ngsama 2025-11-24 16:40:41 +08:00
parent 82cfb5795b
commit a4d67be103
6 changed files with 311 additions and 66 deletions

View file

@ -3,9 +3,18 @@
CC = gcc
CFLAGS = -Wall -Wextra -O2 -std=c11 -D_XOPEN_SOURCE=700
LDFLAGS = -pthread
LDFLAGS = -pthread -lssh
INCLUDES = -Iinclude
# Detect libssh location (homebrew on macOS)
ifeq ($(shell uname), Darwin)
LIBSSH_PREFIX := $(shell brew --prefix libssh 2>/dev/null)
ifneq ($(LIBSSH_PREFIX),)
INCLUDES += -I$(LIBSSH_PREFIX)/include
LDFLAGS += -L$(LIBSSH_PREFIX)/lib
endif
endif
SRC_DIR = src
INC_DIR = include
OBJ_DIR = obj

View file

@ -12,13 +12,30 @@
- 🕐 **Full timestamps** - Year-month-day hour:minute with timezone
- 📖 **Bilingual help** - Press ? for Chinese/English help
- 🌏 **UTF-8 support** - Full support for Chinese, Japanese, Korean
- 📦 **Single binary** - Lightweight ~50KB executable
- 🚀 **Telnet access** - No client installation needed
- 📦 **Single binary** - Lightweight executable
- 🔒 **SSH access** - Secure encrypted connections
- 🖥️ **Auto terminal detection** - Adapts to your terminal size
- 💾 **Message persistence** - All messages saved to log file
- ⚡ **Low resource usage** - Minimal memory and CPU
## Building
**Dependencies:**
- libssh (required for SSH support)
**Install dependencies:**
```bash
# On macOS
brew install libssh
# On Ubuntu/Debian
sudo apt-get install libssh-dev
# On Fedora/RHEL
sudo dnf install libssh-devel
```
**Build:**
```bash
make
```
@ -36,9 +53,11 @@ make debug
Connect from another terminal:
```bash
telnet localhost 2222
ssh -p 2222 localhost
```
The server will prompt for a password (any password is accepted) and then ask for your username.
## Usage
### Operating Modes
@ -78,10 +97,11 @@ telnet localhost 2222
## Architecture
- **Network**: Multi-threaded TCP server
- **TUI**: ANSI escape sequences
- **Network**: Multi-threaded SSH server using libssh
- **TUI**: ANSI escape sequences with automatic terminal size detection
- **Storage**: Append-only log file
- **Concurrency**: pthread + rwlock
- **Security**: Encrypted SSH connections with host key authentication
## Configuration
@ -98,6 +118,16 @@ PORT=3333 ./tnt
- Thread-safe operations
- Proper UTF-8 handling for CJK characters
- Box-drawing characters for UI
- SSH protocol with PTY support for terminal size detection
- Dynamic window resize handling
## Security
- **Encrypted connections**: All traffic is encrypted via SSH
- **Host key authentication**: RSA host key generated on first run
- **Password authentication**: Currently accepts any password (customize for production)
- **Host key persistence**: Stored in `host_key` file
- **No plaintext**: Unlike telnet, all data is encrypted in transit
## License

View file

@ -3,10 +3,14 @@
#include "common.h"
#include "chat_room.h"
#include <libssh/libssh.h>
#include <libssh/server.h>
/* Client connection structure */
typedef struct client {
int fd; /* Socket file descriptor */
int fd; /* Socket file descriptor (not used with SSH) */
ssh_session session; /* SSH session */
ssh_channel channel; /* SSH channel */
char username[MAX_USERNAME_LEN];
int width;
int height;

View file

@ -20,7 +20,7 @@ void tui_render_command_output(struct client *client);
void tui_render_input(struct client *client, const char *input);
/* Clear the screen */
void tui_clear_screen(int fd);
void tui_clear_screen(struct client *client);
/* Get help text based on language */
const char* tui_get_help_text(help_lang_t lang);

View file

@ -1,6 +1,9 @@
#include "ssh_server.h"
#include "tui.h"
#include "utf8.h"
#include <libssh/libssh.h>
#include <libssh/server.h>
#include <libssh/callbacks.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
@ -9,11 +12,59 @@
#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <sys/stat.h>
/* Send data to client */
/* Global SSH bind instance */
static ssh_bind g_sshbind = NULL;
/* Generate or load SSH host key */
static int setup_host_key(ssh_bind sshbind) {
struct stat st;
/* Check if host key exists */
if (stat(HOST_KEY_FILE, &st) == 0) {
/* Load existing key */
if (ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY, HOST_KEY_FILE) < 0) {
fprintf(stderr, "Failed to load host key: %s\n", ssh_get_error(sshbind));
return -1;
}
return 0;
}
/* Generate new key */
printf("Generating new RSA host key...\n");
ssh_key key;
if (ssh_pki_generate(SSH_KEYTYPE_RSA, 2048, &key) < 0) {
fprintf(stderr, "Failed to generate RSA key\n");
return -1;
}
/* Export key to file */
if (ssh_pki_export_privkey_file(key, NULL, NULL, NULL, HOST_KEY_FILE) < 0) {
fprintf(stderr, "Failed to export host key\n");
ssh_key_free(key);
return -1;
}
ssh_key_free(key);
/* Set restrictive permissions */
chmod(HOST_KEY_FILE, 0600);
/* Load the newly created key */
if (ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY, HOST_KEY_FILE) < 0) {
fprintf(stderr, "Failed to load host key: %s\n", ssh_get_error(sshbind));
return -1;
}
return 0;
}
/* Send data to client via SSH channel */
int client_send(client_t *client, const char *data, size_t len) {
if (!client || !client->connected) return -1;
ssize_t sent = write(client->fd, data, len);
if (!client || !client->connected || !client->channel) return -1;
int sent = ssh_channel_write(client->channel, data, len);
return (sent < 0) ? -1 : 0;
}
@ -31,13 +82,13 @@ int client_printf(client_t *client, const char *fmt, ...) {
static int read_username(client_t *client) {
char username[MAX_USERNAME_LEN] = {0};
int pos = 0;
unsigned char buf[4];
char buf[4];
tui_clear_screen(client->fd);
tui_clear_screen(client);
client_printf(client, "请输入用户名: ");
while (1) {
ssize_t n = read(client->fd, buf, 1);
int n = ssh_channel_read(client->channel, buf, 1, 0);
if (n <= 0) return -1;
unsigned char b = buf[0];
@ -57,20 +108,20 @@ static int read_username(client_t *client) {
if (pos < MAX_USERNAME_LEN - 1) {
username[pos++] = b;
username[pos] = '\0';
write(client->fd, &b, 1);
client_send(client, &b, 1);
}
} else {
/* UTF-8 multi-byte */
int len = utf8_byte_length(b);
buf[0] = b;
if (len > 1) {
read(client->fd, &buf[1], len - 1);
ssh_channel_read(client->channel, &buf[1], len - 1, 0);
}
if (pos + len < MAX_USERNAME_LEN - 1) {
memcpy(username + pos, buf, len);
pos += len;
username[pos] = '\0';
write(client->fd, buf, len);
client_send(client, buf, len);
}
}
}
@ -286,11 +337,9 @@ static void handle_key(client_t *client, unsigned char key, char *input) {
void* client_handle_session(void *arg) {
client_t *client = (client_t*)arg;
char input[MAX_MESSAGE_LEN] = {0};
unsigned char buf[4];
char buf[4];
/* Get terminal size (assume 80x24 for telnet) */
client->width = 80;
client->height = 24;
/* Terminal size already set from PTY request */
client->mode = MODE_INSERT;
client->help_lang = LANG_ZH;
client->connected = true;
@ -318,8 +367,8 @@ void* client_handle_session(void *arg) {
tui_render_screen(client);
/* Main input loop */
while (client->connected) {
ssize_t n = read(client->fd, buf, 1);
while (client->connected && ssh_channel_is_open(client->channel)) {
int n = ssh_channel_read(client->channel, buf, 1, 0);
if (n <= 0) break;
unsigned char b = buf[0];
@ -344,7 +393,7 @@ void* client_handle_session(void *arg) {
int char_len = utf8_byte_length(b);
buf[0] = b;
if (char_len > 1) {
read(client->fd, &buf[1], char_len - 1);
ssh_channel_read(client->channel, &buf[1], char_len - 1, 0);
}
int len = strlen(input);
if (len + char_len < MAX_MESSAGE_LEN - 1) {
@ -380,73 +429,225 @@ cleanup:
room_broadcast(g_room, &leave_msg);
}
close(client->fd);
if (client->channel) {
ssh_channel_close(client->channel);
ssh_channel_free(client->channel);
}
if (client->session) {
ssh_disconnect(client->session);
ssh_free(client->session);
}
free(client);
return NULL;
}
/* Initialize server socket */
/* Handle SSH authentication */
static int handle_auth(ssh_session session) {
ssh_message message;
do {
message = ssh_message_get(session);
if (!message) break;
if (ssh_message_type(message) == SSH_REQUEST_AUTH) {
if (ssh_message_subtype(message) == SSH_AUTH_METHOD_PASSWORD) {
/* Accept any password for simplicity */
/* In production, you'd want to verify against a user database */
ssh_message_auth_reply_success(message, 0);
ssh_message_free(message);
return 0;
} else if (ssh_message_subtype(message) == SSH_AUTH_METHOD_NONE) {
/* Deny and ask for password */
ssh_message_auth_set_methods(message, SSH_AUTH_METHOD_PASSWORD);
}
}
ssh_message_reply_default(message);
ssh_message_free(message);
} while (1);
return -1;
}
/* Handle SSH channel requests */
static ssh_channel handle_channel_open(ssh_session session) {
ssh_message message;
ssh_channel channel = NULL;
do {
message = ssh_message_get(session);
if (!message) break;
if (ssh_message_type(message) == SSH_REQUEST_CHANNEL_OPEN &&
ssh_message_subtype(message) == SSH_CHANNEL_SESSION) {
channel = ssh_message_channel_request_open_reply_accept(message);
ssh_message_free(message);
return channel;
}
ssh_message_reply_default(message);
ssh_message_free(message);
} while (1);
return NULL;
}
/* Handle PTY request and get terminal size */
static int handle_pty_request(ssh_channel channel, client_t *client) {
ssh_message message;
do {
message = ssh_message_get(ssh_channel_get_session(channel));
if (!message) break;
if (ssh_message_type(message) == SSH_REQUEST_CHANNEL) {
if (ssh_message_subtype(message) == SSH_CHANNEL_REQUEST_PTY) {
/* Get terminal dimensions from PTY request */
client->width = ssh_message_channel_request_pty_width(message);
client->height = ssh_message_channel_request_pty_height(message);
/* Default to 80x24 if invalid */
if (client->width <= 0 || client->width > 500) client->width = 80;
if (client->height <= 0 || client->height > 200) client->height = 24;
ssh_message_channel_request_reply_success(message);
ssh_message_free(message);
return 0;
} else if (ssh_message_subtype(message) == SSH_CHANNEL_REQUEST_SHELL) {
ssh_message_channel_request_reply_success(message);
ssh_message_free(message);
return 0;
} else if (ssh_message_subtype(message) == SSH_CHANNEL_REQUEST_WINDOW_CHANGE) {
/* Handle terminal resize */
client->width = ssh_message_channel_request_pty_width(message);
client->height = ssh_message_channel_request_pty_height(message);
if (client->width <= 0 || client->width > 500) client->width = 80;
if (client->height <= 0 || client->height > 200) client->height = 24;
/* Re-render screen with new dimensions */
if (client->connected) {
tui_render_screen(client);
}
ssh_message_free(message);
continue;
}
}
ssh_message_reply_default(message);
ssh_message_free(message);
} while (1);
return -1;
}
/* Initialize SSH server */
int ssh_server_init(int port) {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
g_sshbind = ssh_bind_new();
if (!g_sshbind) {
fprintf(stderr, "Failed to create SSH bind\n");
return -1;
}
/* Set socket options */
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind");
close(listen_fd);
/* Set up host key */
if (setup_host_key(g_sshbind) < 0) {
ssh_bind_free(g_sshbind);
return -1;
}
if (listen(listen_fd, 10) < 0) {
perror("listen");
close(listen_fd);
/* Bind to port */
ssh_bind_options_set(g_sshbind, SSH_BIND_OPTIONS_BINDPORT, &port);
ssh_bind_options_set(g_sshbind, SSH_BIND_OPTIONS_BINDADDR, "0.0.0.0");
/* Set verbose level for debugging */
int verbosity = SSH_LOG_WARNING;
ssh_bind_options_set(g_sshbind, SSH_BIND_OPTIONS_LOG_VERBOSITY, &verbosity);
if (ssh_bind_listen(g_sshbind) < 0) {
fprintf(stderr, "Failed to bind to port %d: %s\n", port, ssh_get_error(g_sshbind));
ssh_bind_free(g_sshbind);
return -1;
}
return listen_fd;
return 0;
}
/* Start server (blocking) */
int ssh_server_start(int listen_fd) {
printf("TNT chat server listening on port %d\n", DEFAULT_PORT);
printf("Connect with: telnet localhost %d\n", DEFAULT_PORT);
/* Start SSH server (blocking) */
int ssh_server_start(int unused) {
(void)unused;
printf("TNT chat server listening on port %d (SSH)\n", DEFAULT_PORT);
printf("Connect with: ssh -p %d localhost\n", DEFAULT_PORT);
while (1) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
ssh_session session = ssh_new();
if (!session) {
fprintf(stderr, "Failed to create SSH session\n");
continue;
}
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
if (errno == EINTR) continue;
perror("accept");
/* Accept connection */
if (ssh_bind_accept(g_sshbind, session) != SSH_OK) {
fprintf(stderr, "Error accepting connection: %s\n", ssh_get_error(g_sshbind));
ssh_free(session);
continue;
}
/* Perform key exchange */
if (ssh_handle_key_exchange(session) != SSH_OK) {
fprintf(stderr, "Key exchange failed: %s\n", ssh_get_error(session));
ssh_disconnect(session);
ssh_free(session);
continue;
}
/* Handle authentication */
if (handle_auth(session) < 0) {
fprintf(stderr, "Authentication failed\n");
ssh_disconnect(session);
ssh_free(session);
continue;
}
/* Open channel */
ssh_channel channel = handle_channel_open(session);
if (!channel) {
fprintf(stderr, "Failed to open channel\n");
ssh_disconnect(session);
ssh_free(session);
continue;
}
/* Create client structure */
client_t *client = calloc(1, sizeof(client_t));
if (!client) {
close(client_fd);
ssh_channel_close(channel);
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
continue;
}
client->fd = client_fd;
client->session = session;
client->channel = channel;
client->fd = -1; /* Not used with SSH */
/* Handle PTY request and get terminal size */
if (handle_pty_request(channel, client) < 0) {
/* Set defaults if PTY request fails */
client->width = 80;
client->height = 24;
}
/* Create thread for client */
pthread_t thread;
if (pthread_create(&thread, NULL, client_handle_session, client) != 0) {
close(client_fd);
ssh_channel_close(channel);
ssh_channel_free(channel);
ssh_disconnect(session);
ssh_free(session);
free(client);
continue;
}

View file

@ -6,9 +6,10 @@
#include <unistd.h>
/* Clear the screen */
void tui_clear_screen(int fd) {
void tui_clear_screen(client_t *client) {
if (!client || !client->connected) return;
const char *clear = ANSI_CLEAR ANSI_HOME;
write(fd, clear, strlen(clear));
client_send(client, clear, strlen(clear));
}
/* Render the main screen */