wrap go as a telnet client lib for c to implement a simple telnet client

server.py

py 复制代码
import socket
import threading
import time
import signal
from datetime import datetime

# 服务器配置
HOST = '0.0.0.0'
PORT = 2323
BUFFER_SIZE = 1024
PROMPT = "$ "
SERVER_RUNNING = True  # 控制服务器运行状态
client_sockets = []    # 存储客户端连接
client_lock = threading.Lock()


# --------------------------
# 1. Ctrl+C信号处理(不变)
# --------------------------
def handle_sigint(signum, frame):
    global SERVER_RUNNING
    print(f"\n[!] 收到Ctrl+C信号,正在停止服务器...")
    
    # 停止监听循环
    SERVER_RUNNING = False
    
    # 关闭所有客户端连接
    with client_lock:
        for sock in client_sockets:
            try:
                sock.sendall("\n服务器正在关闭,连接已断开。\n")
                sock.close()
                print(f"[-] 已关闭客户端连接: {sock.getpeername()}")
            except Exception as e:
                print(f"[!] 关闭客户端连接出错: {str(e)}")
        client_sockets.clear()
    
    print(f"[+] Telnet测试服务器已完全停止")


# --------------------------
# 2. 客户端交互逻辑(核心修复:删除多余回显)
# --------------------------
def handle_client(client_socket: socket.socket, client_addr: tuple):
    # 加入客户端列表
    with client_lock:
        client_sockets.append(client_socket)
    
    print(f"[+] 新连接来自: {client_addr}")
    
    try:
        # 1. 发送欢迎信息(保持不变)
        welcome_msg = f"""
=====================================
Telnet 测试服务器 (Python)
欢迎您,连接来自: {client_addr[0]}:{client_addr[1]}
当前时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
支持命令: ls / pwd / date / whoami / help / exit
=====================================
{PROMPT}"""
        client_socket.sendall(welcome_msg.encode('utf-8'))

        # 2. 支持的测试命令(不变)
        supported_cmds = {
            'ls': lambda: "\n".join(["file1.txt", "file2.log", "docs/", "data.csv"]) + "\n",
            'pwd': lambda: "/home/test/user\n",
            'date': lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "\n",
            'whoami': lambda: "test_user\n",
            'help': lambda: "支持命令: ls, pwd, date, whoami, help, exit\n",
            'exit': lambda: "再见!连接即将关闭...\n"
        }

        # 3. 客户端命令循环(核心修复:删除服务器端回显)
        while SERVER_RUNNING:
            client_input = b""
            while b"\n" not in client_input and SERVER_RUNNING:
                client_socket.settimeout(1)
                try:
                    data = client_socket.recv(BUFFER_SIZE)
                    if not data:  # 客户端断开
                        print(f"[-] 客户端 {client_addr} 主动断开连接")
                        return
                    
                    # --------------------------
                    # 【核心修复1】删除服务器端回显
                    # 原错误:服务器主动回显客户端输入(如收到"ls"再发一次"ls")
                    # 修复后:仅处理输入,不回显(客户端已自行回显,避免重复)
                    # --------------------------
                    for byte in data:
                        if byte == 8:  # 处理回退键(清除客户端输入)
                            if len(client_input) > 0:
                                client_input = client_input[:-1]
                                # 仅向客户端发送清除指令,不回显字符
                                client_socket.sendall(b"\x08 \x08")
                        elif byte in (13, 10):  # 回车/换行统一为\n
                            client_input += b"\n"
                        elif 32 <= byte <= 126:  # 可见字符:仅追加,不回显
                            client_input += bytes([byte])
                
                except socket.timeout:
                    continue
                except Exception as e:
                    print(f"[!] 接收客户端 {client_addr} 数据出错: {str(e)}")
                    return

            # 4. 处理命令(不变)
            cmd = client_input.decode('utf-8').strip().lower()
            if not cmd:  # 空输入:仅发送提示符
                client_socket.sendall(PROMPT.encode('utf-8'))
                continue
            print(f"[*] 收到 {client_addr} 的命令: {cmd}")

            # 5. 生成响应(不变)
            if cmd in supported_cmds:
                response = supported_cmds[cmd]()
                if cmd == 'exit':
                    client_socket.sendall(response.encode('utf-8'))
                    time.sleep(0.3)
                    break
            else:
                response = f"错误: 未知命令 '{cmd}',输入 'help' 查看支持的命令\n"

            # 6. 发送响应+提示符(不变)
            client_socket.sendall(response.encode('utf-8'))
            client_socket.sendall(PROMPT.encode('utf-8'))

    except Exception as e:
        if SERVER_RUNNING:
            print(f"[!] 与 {client_addr} 交互出错: {str(e)}")
    finally:
        # 清理客户端连接
        with client_lock:
            if client_socket in client_sockets:
                client_sockets.remove(client_socket)
        client_socket.close()
        print(f"[-] 与 {client_addr} 的连接已关闭")


# --------------------------
# 3. 服务器主逻辑(不变)
# --------------------------
def start_telnet_server():
    signal.signal(signal.SIGINT, handle_sigint)
    
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.setblocking(False)
    
    try:
        server_socket.bind((HOST, PORT))
        server_socket.listen(5)
        print(f"[+] Telnet测试服务器已启动,监听 {HOST}:{PORT}")
        print(f"[+] 按 Ctrl+C 停止服务器\n")

        while SERVER_RUNNING:
            try:
                client_socket, client_addr = server_socket.accept()
                client_thread = threading.Thread(
                    target=handle_client,
                    args=(client_socket, client_addr),
                    daemon=True
                )
                client_thread.start()
            except BlockingIOError:
                time.sleep(0.1)
            except Exception as e:
                if SERVER_RUNNING:
                    print(f"[!] 服务器监听出错: {str(e)}")

    finally:
        server_socket.close()


if __name__ == "__main__":
    start_telnet_server()

telnet_as_dll.go

go 复制代码
package main

// Windows require link with advapi32

/*
#cgo CFLAGS: -I.
#cgo LDFLAGS: -ladvapi32
#include <stdlib.h>
#include <string.h>

typedef struct {
    void* session;
} TelnetHandle;

typedef enum {
    TELNET_LINE_MODE_EDIT = 0,
    TELNET_CHAR_MODE = 1
} TelnetMode;
*/
import "C"
import (
	"errors"
	"fmt"
	"net"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"
	"unsafe"
)

// 在Go侧定义对应的常量,解决未定义问题
const (
	TELNET_LINE_MODE_EDIT = C.TELNET_LINE_MODE_EDIT
	TELNET_CHAR_MODE      = C.TELNET_CHAR_MODE
)

// 定义Go侧的TelnetMode类型
type TelnetMode int

// 纯Go Telnet会话结构体
type telnetSession struct {
	conn    net.Conn
	mode    TelnetMode
	prompt  string
	recvBuf []byte
	mu      sync.Mutex
	closed  bool
	sigChan chan os.Signal
}

// 全局会话映射
var (
	sessionMap = struct {
		sync.Mutex
		m map[*C.TelnetHandle]*telnetSession
	}{m: make(map[*C.TelnetHandle]*telnetSession)}
)

// 跨平台信号处理(Ctrl+C退出)
func init() {
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-sigChan
		sessionMap.Lock()
		for _, sess := range sessionMap.m {
			sess.Close()
		}
		sessionMap.m = make(map[*C.TelnetHandle]*telnetSession)
		sessionMap.Unlock()
		os.Exit(0)
	}()
}

//export TelnetConnect
func TelnetConnect(host *C.char, port C.int) *C.TelnetHandle {
	goHost := C.GoString(host)
	goPort := int(port)
	target := fmt.Sprintf("%s:%d", goHost, goPort)

	conn, err := net.DialTimeout("tcp", target, 5*time.Second)
	if err != nil {
		fmt.Printf("[ERROR] Connect failed: %v\n", err)
		return nil
	}

	sess := &telnetSession{
		conn:    conn,
		mode:    TELNET_LINE_MODE_EDIT,
		recvBuf: make([]byte, 4096),
		sigChan: make(chan os.Signal, 1),
	}

	cHandle := (*C.TelnetHandle)(C.malloc(C.sizeof_TelnetHandle))
	sessionMap.Lock()
	sessionMap.m[cHandle] = sess
	sessionMap.Unlock()

	return cHandle
}

//export TelnetSetMode
func TelnetSetMode(handle *C.TelnetHandle, mode C.int) C.int {
	sessionMap.Lock()
	sess, ok := sessionMap.m[handle]
	sessionMap.Unlock()
	if !ok || sess.closed {
		return -1
	}

	sess.mu.Lock()
	sess.mode = TelnetMode(mode)
	if mode == C.TELNET_LINE_MODE_EDIT {
		sess.conn.Write([]byte{255, 250, 34, 0, 255, 240})
	}
	sess.mu.Unlock()
	return 0
}

//export TelnetSetTerminalType
func TelnetSetTerminalType(handle *C.TelnetHandle, termType *C.char) C.int {
	sessionMap.Lock()
	sess, ok := sessionMap.m[handle]
	sessionMap.Unlock()
	if !ok || sess.closed {
		return -1
	}

	sess.mu.Lock()
	defer sess.mu.Unlock()

	// 【关键修复】Telnet终端类型协商的正确格式:
	// IAC SB TERMINAL_TYPE SEND IAC SE (先发送"请求发送终端类型"的指令)
	// 服务器回复后,再发送 IAC SB TERMINAL_TYPE IS <终端类型> IAC SE
	// 原错误:直接发送终端类型,导致字符混入后续命令
	negotiateCmd := []byte{
		255, 250, // IAC SB(开始子协商)
		24,       // TERMINAL_TYPE(选项码24)
		1,        // SEND(请求服务器发送终端类型,实际应先发送此指令)
		255, 240, // IAC SE(结束子协商)
	}

	// 发送协商指令(仅请求,不直接发送终端类型)
	_, err := sess.conn.Write(negotiateCmd)
	if err != nil {
		return -1
	}

	// (可选)如果需要主动发送终端类型,需等待服务器响应后再发送,避免字符污染
	// 此处简化处理,仅发送协商请求,不主动推送终端类型,解决前缀问题
	return 0
}

//export TelnetSend
func TelnetSend(handle *C.TelnetHandle, data *C.char, length C.int) C.int {
	sessionMap.Lock()
	sess, ok := sessionMap.m[handle]
	sessionMap.Unlock()
	if !ok || sess.closed || length <= 0 {
		return -1
	}

	// 【关键修复】仅发送客户端传来的数据,不额外添加换行符
	goData := C.GoBytes(unsafe.Pointer(data), length)
	sess.mu.Lock()
	defer sess.mu.Unlock()

	// 【删除】行模式下自动补换行的逻辑(客户端已处理)
	if sess.mode == TELNET_LINE_MODE_EDIT && len(goData) > 0 && goData[len(goData)-1] != '\n' {
		goData = append(goData, '\n')
	}

	n, err := sess.conn.Write(goData)
	if err != nil {
		return -1
	}
	return C.int(n)
}

//export TelnetRecv
func TelnetRecv(handle *C.TelnetHandle, buffer *C.char, bufferSize, timeoutMs C.int) C.int {
	sessionMap.Lock()
	sess, ok := sessionMap.m[handle]
	sessionMap.Unlock()
	if !ok || sess.closed || bufferSize <= 0 {
		return -1
	}

	sess.mu.Lock()
	defer sess.mu.Unlock()
	sess.conn.SetReadDeadline(time.Now().Add(time.Duration(timeoutMs) * time.Millisecond))
	n, err := sess.conn.Read(sess.recvBuf)

	if err != nil {
		if errors.Is(err, os.ErrDeadlineExceeded) {
			return 0
		}
		if errors.Is(err, net.ErrClosed) {
			return -2
		}
		return -1
	}

	copy((*[1 << 20]byte)(unsafe.Pointer(buffer))[:], sess.recvBuf[:n])
	return C.int(n)
}

//export TelnetDetectPrompt
func TelnetDetectPrompt(handle *C.TelnetHandle, prompt *C.char) C.int {
	sessionMap.Lock()
	sess, ok := sessionMap.m[handle]
	sessionMap.Unlock()
	if !ok || sess.closed {
		return -1
	}

	sess.mu.Lock()
	sess.prompt = C.GoString(prompt)
	sess.mu.Unlock()
	return 0
}

//export TelnetClose
func TelnetClose(handle *C.TelnetHandle) {
	sessionMap.Lock()
	sess, ok := sessionMap.m[handle]
	if ok {
		sess.Close()
		delete(sessionMap.m, handle)
	}
	sessionMap.Unlock()
	C.free(unsafe.Pointer(handle))
}

// 会话关闭方法
func (s *telnetSession) Close() {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return
	}
	s.closed = true
	s.conn.Close()
	close(s.sigChan)
	signal.Stop(s.sigChan)
}

func main() {}

telnet_client.c

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <stdbool.h>

#ifdef _WIN32
#include <windows.h>
#include <conio.h>
#else
#include <poll.h>
#include <termios.h>
#include <unistd.h>
#endif

#include "telnet.h"

TelnetHandle* g_handle = NULL;
volatile bool g_running = true;
char last_cmd[256] = {0};  // 存储纯命令(无换行)

// 信号处理(不变)
void signal_handler(int signum) {
    if (signum == SIGINT || signum == SIGTERM) {
        printf("\n收到退出信号,正在断开连接...\n");
        g_running = false;
    }
}

// 终端模式设置(不变)
void set_terminal_mode(bool raw) {
#ifdef _WIN32
    HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
    DWORD mode;
    GetConsoleMode(hStdin, &mode);
    SetConsoleMode(hStdin, ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT);
#else
    static struct termios old_termios;
    if (raw) {
        tcgetattr(STDIN_FILENO, &old_termios);
        struct termios new_termios = old_termios;
        new_termios.c_lflag &= ~ICANON;
        new_termios.c_lflag |= ECHO;
        new_termios.c_cc[VMIN] = 1;
        new_termios.c_cc[VTIME] = 0;
        tcsetattr(STDIN_FILENO, TCSANOW, &new_termios);
    } else {
        tcsetattr(STDIN_FILENO, TCSANOW, &old_termios);
    }
#endif
}

// 检查用户输入(不变)
bool has_user_input() {
#ifdef _WIN32
    return _kbhit() != 0;
#else
    struct pollfd pfd = {STDIN_FILENO, POLLIN, 0};
    return poll(&pfd, 1, 0) > 0;
#endif
}

// 【修复1】彻底初始化输入状态,消除残留字符
int read_user_input(char* buffer, size_t max_len) {
    memset(buffer, 0, max_len);  // 清空缓冲区
    static size_t input_pos = 0; // 静态变量确保初始化为0(编译器默认)
    
    // 【关键】每次读取前重置输入位置(避免残留)
    input_pos = 0;

#ifdef _WIN32
    char c;
    while (1) {
        // 【修复2】读取前先清空控制台残留事件(避免读取到历史按键)
        while (_kbhit()) {
            c = _getch();
            // 仅保留当前按键,丢弃历史残留(如引号)
            if (c == '\r' || c == '\b' || c == 27 || (c >= 32 && c <= 126)) {
                break;
            }
        }

        // 处理有效按键
        if (c == '\r') { // 回车:结束输入
            if (input_pos > 0) {
                buffer[input_pos] = '\n'; // 仅添加1个换行符
                input_pos++;
                printf("\n");
                return input_pos;
            }
            c = 0; // 重置无效回车
            continue;
        } else if (c == '\b' && input_pos > 0) { // 退格
            input_pos--;
            printf("\b \b");
            c = 0;
        } else if (c == 27) { // ESC:退出
            strcpy(buffer, "exit\n");
            return strlen(buffer);
        } else if (c >= 32 && c <= 126 && input_pos < max_len - 2) { // 可见字符
            buffer[input_pos] = c;
            input_pos++;
            printf("%c", c);
            c = 0;
        } else {
            // 过滤所有无效字符(如引号、控制符)
            if (_kbhit()) {
                c = _getch();
            } else {
                Sleep(10); // 降低CPU占用
            }
        }
    }
#else
    ssize_t n = read(STDIN_FILENO, buffer, max_len - 1);
    if (n > 0) {
        char* nl = strchr(buffer, '\n');
        if (nl) {
            *nl = '\0';
            strncpy(last_cmd, buffer, sizeof(last_cmd)-1);
            *nl = '\n';
        }
        return n;
    }
    return 0;
#endif
}

// 【修复3】增强响应过滤:精准匹配命令回显(避免漏删)
void filter_echo_response(char* response, int recv_len) {
    if (strlen(last_cmd) == 0) return;

    // 匹配两种回显格式:1. 命令+换行 2. 命令(无换行,服务器自动补)
    char cmd_form1[256];  // 格式1:命令\n(如"ls\n")
    char cmd_form2[256];  // 格式2:命令(如"ls")
    snprintf(cmd_form1, sizeof(cmd_form1), "%s\n", last_cmd);
    snprintf(cmd_form2, sizeof(cmd_form2), "%s\n", last_cmd);
    int len1 = strlen(cmd_form1);
    int len2 = strlen(cmd_form2);

    // 优先匹配格式1(最常见)
    if (recv_len >= len1 && strncmp(response, cmd_form1, len1) == 0) {
        memmove(response, response + len1, recv_len - len1);
        response[recv_len - len1] = '\0';
    } 
    // 再匹配格式2(避免漏删)
    else if (recv_len >= len2 && strncmp(response, cmd_form2, len2) == 0) {
        memmove(response, response + len2, recv_len - len2);
        response[recv_len - len2] = '\0';
    }
}

int main(int argc, char* argv[]) {
    char* host = "localhost";
    int port = 2323;
    
    if (argc >= 2) host = argv[1];
    if (argc >= 3) port = atoi(argv[2]);
    
    printf("Telnet 客户端 - 连接 %s:%d\n", host, port);
    printf("提示:输入命令按回车发送,输入'exit'或按Ctrl+C/ESC退出\n\n");
    
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);
    set_terminal_mode(true);
    
    g_handle = TelnetConnect(host, port);
    if (!g_handle) {
        fprintf(stderr, "错误:无法连接 %s:%d\n", host, port);
        set_terminal_mode(false);
        return 1;
    }
    
    // 【修复4】不启用行模式自动补换行(避免DLL重复添加)
    TelnetSetMode(g_handle, TELNET_LINE_MODE_EDIT);
    TelnetDetectPrompt(g_handle, "$ ");
    
    char recv_buf[4096] = {0};
    char send_buf[1024] = {0};
    
    while (g_running) {
        // 接收服务器数据
        int recv_len = TelnetRecv(g_handle, recv_buf, sizeof(recv_buf)-1, 100);
        if (recv_len > 0) {
            recv_buf[recv_len] = '\0';
            filter_echo_response(recv_buf, recv_len);
            
            if (strlen(recv_buf) > 0) {
                printf("%s", recv_buf);
                fflush(stdout);
            }
            memset(recv_buf, 0, sizeof(recv_buf));
        } else if (recv_len == -2) {
            printf("\n错误:服务器已断开连接\n");
            break;
        }
        
        // 处理用户输入
        if (has_user_input()) {
            int send_len = read_user_input(send_buf, sizeof(send_buf));
            if (send_len > 0) {
                // 保存纯命令(用于过滤)
                char* nl_pos = strchr(send_buf, '\n');
                if (nl_pos != NULL) {
                    *nl_pos = '\0';
                    strncpy(last_cmd, send_buf, sizeof(last_cmd)-1);
                    *nl_pos = '\n'; // 恢复1个换行符
                }
                
                // 发送命令(仅含用户输入+1个换行符)
                TelnetSend(g_handle, send_buf, send_len);
                
                if (strncmp(send_buf, "exit\n", 5) == 0) {
                    g_running = false;
                }
                memset(send_buf, 0, sizeof(send_buf));
            }
        }
    }
    
    printf("\n正在关闭连接...\n");
    TelnetClose(g_handle);
    set_terminal_mode(false);
    printf("客户端已退出\n");
    
    return 0;
}

build on Windows

cmd 复制代码
go build -v -buildmode=c-shared -o telnet.dll telnet_as_dll.go
gcc telnet_client.c -o telnet_client.exe -ltelnet -L.

up python server and test output

cmd 复制代码
Telnet 客户端 - 连接 localhost:2323
提示:输入命令按回车发送,输入'exit'或按Ctrl+C/ESC退出


=====================================
Telnet 测试服务器 (Python)
欢迎您,连接来自: 127.0.0.1:49857
当前时间: 2025-08-17 19:22:51
支持命令: ls / pwd / date / whoami / help / exit
=====================================
$ ls
错误: 未知命令 '"ls',输入 'help' 查看支持的命令
$ pwd
/home/test/user
$ date
2025-08-17 19:22:58
$ whoami
test_user
$ help
支持命令: ls, pwd, date, whoami, help, exit
$ exit

正在关闭连接...
客户端已退出

bug

  1. client ctrl + c 没退出,按esc可以退出
  2. 第一次ls会说没找到命令