Disclaimer:The user is responsible for any direct or indirect consequences and losses caused by the dissemination and use of the information provided by this public account. The public account and the author do not bear any responsibility for this, and any consequences must be borne by the user!
01
Core Architecture of the Server
In the previous article, the author introduced the basic architecture and client implementation of the TTY Shell. This article will focus on the design and implementation of the server, as well as some technical details of advanced features. Unlike the client, the server needs to handle more complex tasks: creating and managing pseudo-terminals (PTY), starting and monitoring shell processes, handling multiple concurrent client connections, and ensuring the security and stability of the entire system.
1.1 Server Component Hierarchy
The RunShell server consists of the following main components:
1. **Server Class**: The top-level component responsible for network listening and client session management.
2. **ClientSession Class**: The session container for each client connection.
3. **PseudoTerminal Class**: The pseudo-terminal abstraction that provides the ability to interact with actual terminal devices.
4. **Process Class**: The process management abstraction used to create and control shell processes.
The relationships between these components are illustrated in the following diagram:
“`
Server
└── Manages multiple ClientSessions
├── Each session has a PseudoTerminal
└── Each session has a Process (Shell process)
“`
1.2 Server Class Design
The Server class is the core class of the server, responsible for listening on network ports, accepting client connections, and managing client sessions:
class Server {public: explicit Server(const ServerConfig& config = ServerConfig()); ~Server(); // Start the server void start(); // Stop the server void stop(); // Get the number of client connections size_t getClientCount() const;private: ServerConfig config_; std::unique_ptr<network::TcpSocket> server_socket_; std::map<int, std::shared_ptr<ClientSession>> clients_; std::thread accept_thread_; std::atomic<bool> running_; std::mutex clients_mutex_; // Client accept thread void acceptThread(); // Connect back to the host thread void connectBackThread(); // Clean up disconnected clients void cleanupClients(); // Handle new connections void handleNewClient(std::shared_ptr<network::Socket> client_socket); // Initialize as a daemon void initDaemon();};
The Server class implements two operating modes:
1. **Standard Listening Mode**: Listens on a specified port, waiting for client connections.
2. **Connect Back Mode**: Actively connects to a specified host, suitable for penetrating firewalls.
1.3 Client Session Management
Each client connection is represented and managed by a ClientSession object:
class ClientSession {public: ClientSession(std::shared_ptr<network::Socket> socket); ~ClientSession(); // Handle client session void handle(); // Terminate session void terminate(); // Get session status bool isActive() const; bool isAuthenticated() const;private: std::shared_ptr<network::Socket> socket_; std::unique_ptr<crypto::SecureChannel> secure_channel_; std::shared_ptr<platform::PseudoTerminal> pty_; std::shared_ptr<platform::Process> shell_process_; std::thread shell_thread_; std::atomic<bool> active_; std::atomic<bool> authenticated_; std::string term_type_; uint16_t rows_; uint16_t cols_; std::chrono::steady_clock::time_point last_activity_time_; // Methods to handle various messages... bool handleAuthentication(); bool handleShellStart(const protocol::Message& message); bool handleShellData(const protocol::Message& message); bool handleShellResize(const protocol::Message& message); bool handleShellStop(const protocol::Message& message); // Shell I/O forwarding thread void shellIoThread(); // Communication methods bool sendMessage(const protocol::Message& message); bool receiveMessage(protocol::Message& message); // Check if connection is alive bool isConnectionAlive();};
When the server receives a new connection, it creates a new ClientSession object and runs it in a separate thread:
void Server::handleNewClient(std::shared_ptr<network::Socket> client_socket) { try { // Create client session auto session = std::make_shared<ClientSession>(client_socket); // Generate a unique client ID int client_id; { std::lock_guard<std::mutex> lock(clients_mutex_); client_id = clients_.empty() ? 1 : (clients_.rbegin()->first + 1); clients_[client_id] = session; } // Handle session in a separate thread std::thread([this, client_id, session]() { session->handle(); // After the session ends, remove from client list std::lock_guard<std::mutex> lock(clients_mutex_); clients_.erase(client_id); }).detach(); } catch (const std::exception& e) { std::cerr << "Exception handling new client: " << e.what() << std::endl; }}
02
Pseudo Terminal (PTY) and Process Management
The pseudo terminal (Pseudo Terminal, PTY) is the core of implementing remote shell functionality. It provides an interface for remote clients to interact with shell processes with full functionality, including support for cursor movement, command history, tab completion, and other advanced terminal features.
2.1 Pseudo Terminal Abstraction and Implementation
RunShell uses the abstract class `PseudoTerminal` to define the pseudo-terminal interface and provides specific implementations for different platforms:
class PseudoTerminal {public: virtual ~PseudoTerminal() = default; // Create a PTY static std::unique_ptr<PseudoTerminal> create(); // Read and write operations virtual size_t read(std::vector<uint8_t>& buffer, size_t maxSize) = 0; virtual size_t write(const std::vector<uint8_t>& buffer) = 0; // Set terminal size virtual void resize(uint16_t rows, uint16_t cols) = 0; // Get master device file descriptor virtual int getMasterFd() const = 0; // Get slave device name virtual std::string getSlaveName() const = 0; // Close PTY virtual void close() = 0;};
On Unix/Linux platforms, the pseudo terminal is implemented through the `UnixPseudoTerminal` class:
class UnixPseudoTerminal : public PseudoTerminal {public: UnixPseudoTerminal() { // Open pseudo terminal int result = openpty(&master_fd_, &slave_fd_, slave_name_, nullptr, nullptr); if (result != 0) { throw PlatformError("Failed to create pseudo terminal: " + getLastErrorMessage()); } // Get the name of the slave PTY if (ptsname_r(master_fd_, slave_name_, sizeof(slave_name_)) != 0) { throw PlatformError("Failed to get PTY slave device name: " + getLastErrorMessage()); } // Set master to non-blocking mode int flags = fcntl(master_fd_, F_GETFL); fcntl(master_fd_, F_SETFL, flags | O_NONBLOCK); } size_t read(std::vector<uint8_t>& buffer, size_t maxSize) override { buffer.resize(maxSize); // Use poll to check if there is data to read struct pollfd pfd; pfd.fd = master_fd_; pfd.events = POLLIN; int ret = poll(&pfd, 1, 50); // 50 ms timeout if (ret <= 0) { // Timeout or error buffer.clear(); return 0; } ssize_t bytesRead = ::read(master_fd_, buffer.data(), maxSize); if (bytesRead < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // No data to read in non-blocking mode buffer.clear(); return 0; } throw PlatformError("Error reading PTY: " + getLastErrorMessage()); } buffer.resize(bytesRead); return bytesRead; } // Other method implementations...private: int master_fd_; // Master device file descriptor int slave_fd_; // Slave device file descriptor char slave_name_[256]; // Slave device name};
2.2 Process Management
Process management is handled by the `Process` abstract class and platform-specific implementations:
class Process {public: virtual ~Process() = default; // Create a new process static std::unique_ptr<Process> create(const std::string& command, const std::vector<std::string>& args = {}); // Create a process using PTY static std::unique_ptr<Process> createWithPty(std::unique_ptr<PseudoTerminal> pty, const std::string& command, const std::vector<std::string>& args = {}, const std::vector<std::string>& env_vars = {}); // Check if the process is running virtual bool isRunning() const = 0; // Get process ID virtual int getPid() const = 0; // Wait for the process to finish virtual int wait(bool block) = 0; // Terminate the process virtual void terminate() = 0; // Forcefully kill the process virtual void kill() = 0;};
To associate the shell process with the pseudo terminal, we implemented the `UnixPtyProcess` class:
class UnixPtyProcess : public UnixProcess {public: UnixPtyProcess(std::unique_ptr<PseudoTerminal> pty, const std::string& command, const std::vector<std::string>& args, const std::vector<std::string>& env_vars) : UnixProcess(-1), pty_(std::move(pty)) { // Get PTY path std::string slave_name = pty_->getSlaveName(); // Build command line arguments std::vector<char*> argv; argv.push_back(const_cast<char*>(command.c_str())); for (const auto& arg : args) { argv.push_back(const_cast<char*>(arg.c_str())); } argv.push_back(nullptr); // Terminate argument list // Create child process pid_t pid = fork(); if (pid < 0) { throw PlatformError("Failed to create process: " + getLastErrorMessage()); } else if (pid == 0) { // Child process // Create new session setsid(); // Open PTY slave device int slave_fd = open(slave_name.c_str(), O_RDWR); if (slave_fd < 0) { _exit(EXIT_FAILURE); } // Set as control terminal ioctl(slave_fd, TIOCSCTTY, 0); // Redirect standard input/output to PTY dup2(slave_fd, STDIN_FILENO); dup2(slave_fd, STDOUT_FILENO); dup2(slave_fd, STDERR_FILENO); // Close unnecessary file descriptors if (slave_fd > STDERR_FILENO) { close(slave_fd); } // Set environment variables for (const auto& env : env_vars) { putenv(const_cast<char*>(env.c_str())); } // Execute command execvp(command.c_str(), argv.data()); // If we reach here, execution failed _exit(EXIT_FAILURE); } // Parent process setPid(pid); }private: std::unique_ptr<PseudoTerminal> pty_;};
2.3 Terminal Settings and Environment Variables
Before starting the shell process, it is necessary to correctly configure terminal parameters and environment variables:
bool ClientSession::handleShellStart(const protocol::Message& message) { try { // Parse terminal type, rows, and columns auto [term_type, rows, cols] = protocol::ShellSession::parseStartRequest(message); term_type_ = term_type; rows_ = rows; cols_ = cols; // Create PTY pty_ = platform::PseudoTerminal::create(); // Set PTY terminal parameters #ifndef _WIN32 struct termios term_settings; if (tcgetattr(pty_->getMasterFd(), &term_settings) != -1) { // Configure terminal to raw mode to correctly pass all input/output // Keep echo enabled to display input locally term_settings.c_lflag |= ECHO | ECHOE | ECHOK; // Ensure correct conversion of CR and NL term_settings.c_iflag |= ICRNL; term_settings.c_oflag |= ONLCR; // Set control characters term_settings.c_cc[VEOF] = 4; // Ctrl-D term_settings.c_cc[VEOL] = 0; // NUL term_settings.c_cc[VERASE] = 127; // DEL term_settings.c_cc[VINTR] = 3; // Ctrl-C // Apply settings tcsetattr(pty_->getMasterFd(), TCSANOW, &term_settings); } #endif // Set terminal size pty_->resize(rows, cols); // Build environment variables std::vector<std::string> env_vars; env_vars.push_back("TERM=" + term_type); env_vars.push_back("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"); env_vars.push_back("HOME=" + platform::User::getHomeDir()); env_vars.push_back("SHELL=/bin/bash"); env_vars.push_back("LANG=en_US.UTF-8"); // Ensure UTF-8 support // Create shell process using login shell mode shell_process_ = platform::Process::createWithPty( pty_, "/bin/bash", {"--login"}, env_vars); // Start shell I/O thread shell_thread_ = std::thread(&ClientSession::shellIoThread, this); return true; } catch (const std::exception& e) { std::cerr << "Exception starting shell: " << e.what() << std::endl; return false; }}
03
Implementation of Advanced Features
3.1 Shell I/O Thread and Buffer Management
The Shell I/O thread is responsible for forwarding data between the PTY and the client. To improve performance, RunShell implements intelligent buffer management, sending data only when necessary:
void ClientSession::shellIoThread() { try { // Send buffer and timer std::vector<uint8_t> buffer(4096); std::vector<uint8_t> accumulator; const size_t MAX_ACCUMULATOR_SIZE = 1024; // Send when accumulating 1KB of data const int MAX_DELAY_MS = 10; // Or when not sent for more than 10ms auto last_send_time = std::chrono::steady_clock::now(); while (active_) { try { auto current_time = std::chrono::steady_clock::now(); // Check if the shell process has terminated if (shell_process_ && !shell_process_->isRunning()) { // Get exit code int exit_code = shell_process_->wait(false); std::cout << "Shell process has exited, sending stop message to client" << std::endl; // Send stop message to client sendMessage(protocol::ShellSession::createStop(exit_code)); // Terminate session active_ = false; break; } // Use poll to check if there is readable data in the PTY struct pollfd pfd; pfd.fd = pty_->getMasterFd(); pfd.events = POLLIN; int poll_result = poll(&pfd, 1, 1); // 1ms timeout, quick response // Determine if conditions for sending data are met bool timeout = std::chrono::duration_cast<std::chrono::milliseconds>( current_time - last_send_time).count() >= MAX_DELAY_MS; if (poll_result > 0 && (pfd.revents & POLLIN)) { // Data is readable size_t bytes_read = pty_->read(buffer, buffer.size()); if (bytes_read > 0) { // Add the read data to the accumulator accumulator.insert(accumulator.end(), buffer.begin(), buffer.begin() + bytes_read); } } // If enough data has accumulated or the maximum delay time has been reached, send if (!accumulator.empty() && (accumulator.size() >= MAX_ACCUMULATOR_SIZE || timeout)) { // Create and send shell data message protocol::Message data_msg = protocol::ShellSession::createData(accumulator); if (sendMessage(data_msg)) { debug_log("Sent shell data to client (" + std::to_string(accumulator.size()) + " bytes)", false); } // Clear the accumulator and update the send time accumulator.clear(); last_send_time = current_time; } } catch (const std::exception& e) { std::cerr << "Shell IO thread exception: " << e.what() << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); } } } catch (const std::exception& e) { std::cerr << "Fatal error in Shell IO thread: " << e.what() << std::endl; } std::cout << "Shell IO thread has exited" << std::endl;}
3.2 Shell Exit Handling
It is also necessary to ensure that the shell process can correctly notify the client upon exit. RunShell addresses this through the following mechanism:
1. Regularly check the shell process status in the Shell I/O thread.
2. When the shell process exit is detected, send a SHELL_STOP message to the client.
3. The client safely terminates the program upon receiving the SHELL_STOP message.
bool ClientSession::handleShellStop(const protocol::Message& message) { try { // Parse exit code int exit_code = protocol::ShellSession::parseStop(message); // Terminate shell process if (shell_process_ && shell_process_->isRunning()) { shell_process_->terminate(); // Wait for shell to finish shell_process_->wait(true); } // Close PTY if (pty_) { pty_->close(); } // Wait for Shell I/O thread to finish if (shell_thread_.joinable()) { shell_thread_.join(); } // Send confirmation message sendMessage(protocol::ShellSession::createStop(exit_code)); return false; // Return false to terminate session } catch (const std::exception& e) { std::cerr << "Exception stopping shell: " << e.what() << std::endl; return false; }}
3.3 Dynamic Window Size Adjustment
When the client terminal window size changes, the server PTY size needs to be updated to ensure correct display:
bool ClientSession::handleShellResize(const protocol::Message& message) { try { // Parse window size information auto [rows, cols] = protocol::ShellSession::parseResize(message); // Update saved terminal size rows_ = rows; cols_ = cols; // If PTY exists, adjust its size if (pty_) { pty_->resize(rows_, cols_); } return true; } catch (const std::exception& e) { std::cerr << "Exception resizing shell window: " << e.what() << std::endl; return false; }}
On Unix/Linux platforms, PTY resizing is implemented through the `TIOCSWINSZ` ioctl:
void UnixPseudoTerminal::resize(uint16_t rows, uint16_t cols) { struct winsize ws; ws.ws_row = rows; ws.ws_col = cols; ws.ws_xpixel = 0; ws.ws_ypixel = 0; if (ioctl(master_fd_, TIOCSWINSZ, &ws) < 0) { throw PlatformError("Failed to resize PTY: " + getLastErrorMessage()); }}
3.4 Heartbeat Mechanism and Connection Detection
The heartbeat mechanism implemented by RunShell is as follows:
bool ClientSession::isConnectionAlive() { auto now = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast<std::chrono::seconds>( now - last_activity_time_).count(); // If the connection has been inactive for more than 3 seconds, send a heartbeat check if (elapsed > 3) { std::cout << "Connection has been inactive for more than 3 seconds, sending heartbeat check..." << std::endl; protocol::Message ping(protocol::OpCode::HEARTBEAT, {}); if (!sendMessage(ping)) { std::cerr << "Heartbeat check failed to send, assuming connection is broken" << std::endl; return false; } // Reset activity time last_activity_time_ = now; } return true;}
3.5 Daemon Mode and Connect Back Functionality
RunShell supports two operating modes:
1. **Daemon Mode**: Runs in the background without terminal interaction.
2. **Connect Back Mode**: Actively connects to a specified server for penetrating firewalls or NAT.
void Server::start() { if (running_) { return; // Server is already running } // If configured for daemon mode, initialize daemon if (config_.daemonize) { initDaemon(); } // Set process name if (!config_.procName.empty()) { platform::Daemon::setProcessTitle(config_.procName); } // Check PID file if (!config_.pidFile.empty()) { if (platform::Daemon::checkPidFile(config_.pidFile)) { throw std::runtime_error("Server is already running (PID file exists)"); } // Write PID file platform::Daemon::writePidFile(config_.pidFile); } // Choose startup method based on mode if (config_.connectBackMode) { // Connect back mode running_ = true; accept_thread_ = std::thread(&Server::connectBackThread, this); } else { // Normal listening mode // ... }}
The implementation of connect back mode is as follows:
void Server::connectBackThread() { while (running_) { try { // Create client socket auto client_socket = std::make_unique<network::TcpSocket>(); // Attempt to connect to host if (client_socket->connect(config_.connectBackHost, config_.port)) { // Handle new connection handleNewClient(std::move(client_socket)); } // Clean up disconnected clients cleanupClients(); // Wait for a specified time before retrying std::this_thread::sleep_for(std::chrono::seconds(config_.connectBackDelay)); } catch (const std::exception& e) { std::cerr << "Reconnect failed: " << e.what() << std::endl; // Wait for a while before retrying std::this_thread::sleep_for(std::chrono::seconds(config_.connectBackDelay)); } }}
04
Detailed Cross-Platform Adaptation
4.1 Conditional Compilation and Platform Abstraction
RunShell uses conditional compilation and abstract interfaces to handle differences across platforms:
#ifdef RUNSHELL_PLATFORM_WINDOWS // Windows specific code#else // Unix/Linux specific code#endif
For each feature that requires cross-platform support, an abstract base class is defined, and specific implementations are provided for different platforms:
// Abstract base class defining functionality interfaceclass Socket {public: virtual ~Socket() = default; virtual bool isConnected() const = 0; virtual void close() = 0; // ... other methods}; // Unix/Linux platform implementationclass UnixSocket : public Socket { // Implement methods...}; // Windows platform implementationclass WindowsSocket : public Socket { // Implement methods...};
4.2 Special Handling for Windows Platform
The Windows platform has significant differences from Unix/Linux platforms in terminal and network handling. For example:
1. **Terminal Handling**: Windows uses the console API instead of POSIX terminal interfaces.
2. **Network API**: Uses Winsock instead of standard socket API.
3. **Process Creation**: Uses CreateProcess instead of fork/exec.
4. **Pseudo Terminal**: The ConPTY API is only supported in newer versions of Windows 10.
#ifdef _WIN32 #include <winsock2.h> // Windows platform WSA initialization class class WSAInitializer { public: WSAInitializer() { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { throw NetworkError("WSAStartup failed"); } } ~WSAInitializer() { WSACleanup(); } }; // Ensure WSA library is initialized static WSAInitializer wsaInitializer;#endif
4.3 Platform-Specific Error Handling
The error handling mechanisms differ significantly across platforms, and RunShell provides a unified error handling interface:
std::string getLastErrorMessage() {#ifdef _WIN32 DWORD error_code = GetLastError(); if (error_code == 0) { return "No error"; } LPSTR message_buffer = nullptr; size_t size = FormatMessageA( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&message_buffer, 0, NULL); std::string message(message_buffer, size); LocalFree(message_buffer); return message;#else return std::string(strerror(errno));#endif}
05
Performance Optimization and Security Hardening
5.1 I/O Performance Optimization
RunShell employs the following strategies to optimize I/O performance:
1. **Non-blocking I/O Mode**: Avoids thread blocking, improving concurrent processing capability.
2. **Smart Buffering**: Processes small packets in batches to reduce network overhead.
3. **Efficient Polling**: Uses poll instead of busy waiting to reduce CPU usage.
// Set non-blocking modeint flags = fcntl(master_fd_, F_GETFL);fcntl(master_fd_, F_SETFL, flags | O_NONBLOCK); // Smart buffering implementationif (!accumulator.empty() && (accumulator.size() >= MAX_ACCUMULATOR_SIZE || timeout)) { // Batch send data...} // Efficient pollingstruct pollfd pfd;pfd.fd = master_fd_;pfd.events = POLLIN;int poll_result = poll(&pfd, 1, 1); // 1ms timeout
5.2 Encryption Communication Security Hardening
RunShell uses modern security algorithms and key derivation methods to ensure communication security:
1. **Encryption Algorithm**: Uses the ChaCha20-Poly1305 algorithm.
2. **Key Exchange**: Combines pre-shared keys and ephemeral keys to prevent replay attacks.
3. **Session Isolation**: Each client uses an independent encryption channel and key.
4. **Counter Synchronization**: Ensures that the encryption counter is correctly synchronized to prevent decryption failures.
// Key derivationstd::vector<uint8_t> derived_key = crypto::HMAC_SHA256::compute( crypto::HMAC_SHA256::compute(secret_bytes, server_pub_key), nonce); // Reset counterssecure_channel_->resetCounters();std::cout << "Reset encryption/decryption counters: send=0, recv=0" << std::endl;
5.3 Error Detection and Recovery Mechanism
To ensure stability, RunShell implements comprehensive error detection and recovery mechanisms:
1. **Timeout Detection**: Detects and handles network communication timeouts.
2. **Connection Disruption Handling**: Handles connection interruption situations.
3. **Resource Cleanup**: Ensures resources are correctly released under all circumstances.
4. **Exception Safety**: All critical operations are enclosed in try-catch blocks.
try { // Perform operation...} catch (const network::NetworkError& e) { if (e.what() && std::string(e.what()).find("timeout") != std::string::npos) { // Handle timeout error... } else { // Handle other network errors... }} catch (const std::exception& e) { // Handle general exceptions...} catch (...) { // Catch all other exceptions...}
5.4 Architectural Level Security Assurance
In addition to code-level security measures, RunShell also implements architectural level security assurances:
1. **Permission Separation**: Shell processes run with target user permissions rather than server permissions.
2. **Session Isolation**: Each client session is isolated from one another to prevent interference.
3. **Custom Process Names**: Hides the real process name to reduce detection risk.
4. **Log Control**: Configurable log levels to prevent sensitive information leakage.
06
Conclusion
This article provides a detailed introduction to the server implementation and advanced features of the cross-platform TTY Shell. The previous and current articles offer a complete, functional, and secure remote terminal solution.

“Original content is not easy, feel free to share and appreciate”

Follow the public account for more learning materials
