Endlessh源码剖析:C语言网络编程最佳实践

Endlessh源码剖析:C语言网络编程最佳实践

【免费下载链接】endlessh SSH tarpit that slowly sends an endless banner 【免费下载链接】endlessh 项目地址: https://gitcode.com/gh_mirrors/en/endlessh

本文深入剖析了Endlessh SSH蜜罐的源码实现,重点分析了其事件驱动架构、内存管理、信号处理和随机数生成等核心机制。Endlessh采用单线程poll()多路复用技术高效处理数千并发连接,通过精心设计的FIFO队列管理客户端状态,实现了优雅的资源回收和信号处理机制。其随机数生成算法能够产生看似合法的SSH横幅数据,有效拖延攻击者。这些设计体现了C语言网络编程的最佳实践,为开发高性能网络服务提供了优秀范例。

事件驱动架构与poll()机制实现

Endlessh采用经典的事件驱动架构设计,通过单线程高效处理数千个并发连接,其核心在于精心设计的poll()系统调用机制。这种架构避免了多线程的复杂性,同时保证了资源的高效利用。

核心事件循环架构

Endlessh的主事件循环构建在while(running)结构之上,通过poll()系统调用实现多路复用IO。整个事件处理流程如下:

mermaid

poll()机制的精妙实现

Endlessh的poll()调用设计极具巧思,主要体现在以下几个方面:

1. 动态超时计算
int timeout = -1;
long long now = epochms();
while (fifo->head) {
    if (fifo->head->send_next <= now) {
        // 立即处理到期客户端
        struct client *c = fifo_pop(fifo);
        if (sendline(c, config.max_line_length, &rng)) {
            c->send_next = now + config.delay;
            fifo_append(fifo, c);
        }
    } else {
        // 计算下一个到期时间
        timeout = fifo->head->send_next - now;
        break;
    }
}

这种设计确保poll()只在必要时阻塞,最大程度减少CPU空转。

2. 条件性poll调用
struct pollfd fds = {server, POLLIN, 0};
int nfds = fifo->length < config.max_clients;
int r = poll(&fds, nfds, timeout);

nfds参数的精妙之处在于:当客户端数达到上限时,不再监听新连接事件,避免accept()调用失败。

客户端状态管理

Endlessh使用FIFO队列管理客户端状态,每个客户端包含完整的连接信息:

字段类型描述
ipaddrchar[INET6_ADDRSTRLEN]客户端IP地址
connect_timelong long连接建立时间(毫秒)
send_nextlong long下次发送时间戳
bytes_sentlong long已发送字节数
fdint套接字文件描述符
portint客户端端口号

非阻塞IO处理

Endlessh将所有客户端套接字设置为非阻塞模式:

int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

这种设计确保单个客户端的慢速操作不会阻塞整个事件循环。

错误处理与恢复机制

poll()系统调用的错误处理体现了生产级代码的健壮性:

int r = poll(&fds, nfds, timeout);
if (r == -1) {
    switch (errno) {
        case EINTR:
            logmsg(log_debug, "EINTR");
            continue;  // 信号中断,继续循环
        default:
            fprintf(stderr, "endlessh: fatal: %s\n", strerror(errno));
            exit(EXIT_FAILURE);
    }
}

性能优化策略

  1. 最小接收缓冲区:设置SO_RCVBUF为1字节,降低资源消耗
  2. 精确时间管理:使用clock_gettime()获取毫秒级时间戳
  3. 内存高效:单线程避免锁竞争,链表管理避免内存碎片

与传统方案的对比

特性select()poll()epoll()Endlessh方案
连接数限制1024无限制无限制可配置(默认4096)
内存使用中等极低
时间复杂度O(n)O(n)O(1)O(n)但优化
可移植性Linux特有极高

Endlessh的poll()实现展示了如何在保持代码简洁性的同时,实现高性能的网络服务。其设计哲学体现了Unix的"简单而有效"原则,通过精心的事件调度和状态管理,单线程即可处理大量并发连接。

这种架构特别适合SSH tarpit这类IO密集型、计算简单的应用场景,避免了多线程编程的复杂性,同时保证了系统的稳定性和可维护性。

内存管理与资源回收策略

Endlessh作为一个高性能的SSH tarpit服务,其内存管理和资源回收机制设计得极为精巧。在C语言网络编程中,内存泄漏和资源未正确释放是常见问题,但Endlessh通过一系列最佳实践确保了系统的稳定性和可靠性。

客户端内存分配与释放策略

Endlessh采用链表结构管理客户端连接,每个客户端使用malloc动态分配内存,并在连接结束时通过free正确释放:

struct client *client_new(int fd, long long send_next) {
    struct client *c = malloc(sizeof(*c));
    if (c) {
        // 初始化客户端结构体
        c->ipaddr[0] = 0;
        c->connect_time = epochms();
        c->send_next = send_next;
        c->bytes_sent = 0;
        c->next = 0;
        c->fd = fd;
        c->port = 0;
        // ... 其他初始化代码
    }
    return c;
}

static void client_destroy(struct client *client) {
    logmsg(log_debug, "close(%d)", client->fd);
    long long dt = epochms() - client->connect_time;
    logmsg(log_info,
            "CLOSE host=%s port=%d fd=%d "
            "time=%lld.%03lld bytes=%lld",
            client->ipaddr, client->port, client->fd,
            dt / 1000, dt % 1000,
            client->bytes_sent);
    statistics.milliseconds += dt;
    close(client->fd);  // 关闭文件描述符
    free(client);       // 释放内存
}

FIFO队列的资源管理

Endlessh使用FIFO(先进先出)队列来管理客户端连接,确保在程序退出时所有资源都能被正确清理:

static void fifo_destroy(struct fifo *q) {
    struct client *c = q->head;
    while (c) {
        struct client *dead = c;
        c = c->next;
        client_destroy(dead);  // 递归销毁所有客户端
    }
    q->head = q->tail = 0;
    q->length = 0;
}

信号处理与优雅关闭

程序通过信号处理机制实现优雅关闭,确保在收到终止信号时能够正确释放所有资源:

static volatile sig_atomic_t running = 1;

static void sigterm_handler(int signal) {
    (void)signal;
    running = 0;  // 设置运行标志为false,触发主循环退出
}

// 在主循环退出时调用fifo_destroy清理所有资源
fifo_destroy(fifo);

内存管理流程图

mermaid

资源统计与监控

Endlessh还实现了详细的资源使用统计,帮助监控内存和连接状态:

static struct {
    long long connects;
    long long milliseconds;
    long long bytes_sent;
} statistics;

static void statistics_log_totals(struct client *clients) {
    long long milliseconds = statistics.milliseconds;
    for (long long now = epochms(); clients; clients = clients->next)
        milliseconds += now - clients->connect_time;
    logmsg(log_info, "TOTALS connects=%lld seconds=%lld.%03lld bytes=%lld",
           statistics.connects,
           milliseconds / 1000,
           milliseconds % 1000,
           statistics.bytes_sent);
}

最佳实践总结

Endlessh的内存管理策略体现了C语言网络编程的最佳实践:

  1. 一对一分配释放:每个malloc都有对应的free,确保无内存泄漏
  2. 资源所有权明确:FIFO队列明确拥有其包含的客户端资源
  3. 优雅关闭机制:通过信号处理确保程序退出时释放所有资源
  4. 错误处理完备:所有系统调用都有适当的错误检查和日志记录
  5. 统计监控完善:详细记录资源使用情况,便于监控和调试

这种设计使得Endlessh能够在长时间运行中保持稳定的内存使用,即使处理大量并发连接也不会出现内存泄漏问题。

信号处理与优雅关闭机制

Endlessh作为一个长期运行的服务进程,其信号处理机制设计得十分精巧,体现了C语言网络编程中信号处理的最佳实践。该程序支持三种主要信号:SIGTERM用于优雅关闭、SIGHUP用于配置重载、SIGUSR1用于统计信息输出。

信号处理器设计模式

Endlessh采用经典的"标志位+主循环检查"的信号处理模式,这是Unix/Linux系统编程中的推荐做法。信号处理器本身只设置volatile标志变量,实际的信号处理逻辑在主循环中执行,避免了在信号处理器中执行复杂操作可能引发的竞态条件。

static volatile sig_atomic_t running = 1;
static volatile sig_atomic_t reload = 0;
static volatile sig_atomic_t dumpstats = 0;

static void sigterm_handler(int signal) {
    (void)signal;
    running = 0;
}

static void sighup_handler(int signal) {
    (void)signal;
    reload = 1;
}

static void sigusr1_handler(int signal) {
    (void)signal;
    dumpstats = 1;
}

信号处理器安装机制

程序使用sigaction()系统调用安装信号处理器,相比传统的signal()函数,sigaction()提供了更精确的控制和更好的可移植性。每个信号处理器都独立安装,确保错误处理得当。

/* Install the signal handlers */
signal(SIGPIPE, SIG_IGN);  // 忽略SIGPIPE信号
{
    struct sigaction sa = {.sa_handler = sigterm_handler};
    int r = sigaction(SIGTERM, &sa, 0);
    if (r == -1)
        die();
}
// 类似安装SIGHUP和SIGUSR1处理器

主循环中的信号处理逻辑

在主事件循环中,程序定期检查信号标志位,实现信号的异步处理:

mermaid

优雅关闭机制

当接收到SIGTERM信号时,running标志被设置为0,主循环自然退出。程序随后执行清理操作:

  1. 销毁客户端队列:调用fifo_destroy()释放所有客户端资源
  2. 输出最终统计:调用statistics_log_totals()记录运行期间的连接统计
  3. 关闭系统日志:如果使用syslog则调用closelog()
// 主循环退出后的清理代码
fifo_destroy(fifo);
statistics_log_totals(0);

if (logmsg == logsyslog)
    closelog();

EINTR错误处理

在网络I/O操作中,程序正确处理被信号中断的情况:

int r = poll(&fds, nfds, timeout);
if (r == -1) {
    switch (errno) {
        case EINTR:
            logmsg(log_debug, "EINTR");
            continue;  // 被信号中断,继续循环
        default:
            // 处理其他错误
    }
}

配置热重载机制

SIGHUP信号触发配置重载,程序会:

  1. 保存当前端口和绑定族配置
  2. 重新加载配置文件
  3. 如果端口或绑定族发生变化,重新创建服务器套接字
  4. 保持现有客户端连接不受影响

统计信息输出

SIGUSR1信号用于实时监控,输出当前连接统计:

统计项描述数据类型
connects总连接数long long
seconds总连接时长秒.毫秒
bytes总发送字节数long long

最佳实践总结

Endlessh的信号处理机制体现了以下C语言网络编程最佳实践:

  1. 使用volatile sig_atomic_t:确保信号标志变量的原子性和可见性
  2. 避免信号处理器中的复杂操作:保持信号处理器简单,仅设置标志位
  3. 使用sigaction而非signal:提供更可靠的信号处理控制
  4. 正确处理EINTR:确保系统调用被信号中断后能正确恢复
  5. 优雅的资源清理:在程序退出前释放所有分配的资源
  6. 配置热重载:支持运行时配置更新而不中断服务

这种设计模式确保了Endlessh在各种信号场景下都能稳定运行,为其他C语言网络服务开发提供了优秀的参考范例。

随机数生成与防探测算法

Endlessh作为SSH蜜罐的核心功能之一,是通过生成看似合法但实则无效的SSH横幅数据来拖延攻击者。其随机数生成算法和防探测机制的设计体现了C语言网络编程的精妙之处。

线性同余生成器实现

Endlessh采用线性同余生成器(Linear Congruential Generator, LCG)作为其伪随机数生成核心算法。该算法在rand16()函数中实现:

static unsigned
rand16(unsigned long s[1])
{
    s[0] = s[0] * 1103515245UL + 12345UL;
    return (s[0] >> 16) & 0xffff;
}

该LCG算法的参数选择遵循经典的随机数生成器设计:

  • 乘数(Multiplier):1103515245UL
  • 增量(Increment):12345UL
  • 模数(Modulus):2³²(隐式)

算法的数学表达式为:

sₙ₊₁ = (a × sₙ + c) mod m

其中:

  • a = 1103515245
  • c = 12345
  • m = 2³²

随机种子初始化策略

Endlessh采用系统时间作为随机数生成器的种子,确保每次运行都有不同的随机序列:

unsigned long rng = epochms();

epochms()函数返回当前时间的毫秒级时间戳,提供了足够的熵源来初始化随机数生成器。这种设计避免了使用传统的srand(time(NULL))方式,因为:

  1. 时间戳精度更高(毫秒级 vs 秒级)
  2. 减少了系统调用开销
  3. 提供了更好的随机性分布

随机横幅行生成算法

randline()函数负责生成看似合法的SSH横幅行:

static int
randline(char *line, int maxlen, unsigned long s[1])
{
    int len = 3 + rand16(s) % (maxlen - 2);
    for (int i = 0; i < len - 2; i++)
        line[i] = 32 + rand16(s) % 95;
    line[len - 2] = 13;
    line[len - 1] = 10;
    if (memcmp(line, "SSH-", 4) == 0)
        line[0] = 'X';
    return len;
}

该算法的设计特点:

长度随机化

mermaid

字符范围控制

生成的字符范围严格控制在可打印ASCII字符范围内(32-126),确保生成的横幅看起来像合法的文本数据:

字符类型ASCII范围用途
可打印字符32-126主体内容
回车符13行结束
换行符10行结束
防探测机制

关键的防探测逻辑体现在对"SSH-"前缀的检测和替换:

if (memcmp(line, "SSH-", 4) == 0)
    line[0] = 'X';

这个简单的检查防止了生成的随机数据意外形成有效的SSH协议头,从而避免被攻击者识别为真正的SSH服务器。

随机数序列的状态管理

Endlessh采用单全局随机数状态变量,通过指针传递确保状态一致性:

unsigned long rng = epochms();  // 初始化

// 在使用时传递状态指针
if (sendline(c, config.max_line_length, &rng)) {
    // ...
}

这种设计避免了全局变量带来的线程安全问题(虽然Endlessh是单线程的),同时保持了代码的清晰性和可维护性。

性能与随机性权衡

Endlessh在随机数生成方面做出了精心的设计权衡:

设计选择理由优势
使用LCG而非更复杂的PRNG性能要求高,不需要密码学强度计算开销小,速度快
16位随机数输出满足字符生成需求减少计算量,提高效率
时间戳种子简单有效的熵源避免复杂的熵收集机制

实际应用效果

通过这种随机数生成算法,Endlessh能够产生高度逼真的SSH横幅数据:

XjK#pLmN8*RtSvWxYz1 3
AbCdEfGhIjKlMnOpQrS 
Z0!2$4%6&8(0*+-/:=?

这些数据具有以下特征:

  1. 长度变化(3-255字节,默认最大32字节)
  2. 字符组成看似合理
  3. 避免形成有效的协议标识
  4. 包含标准的CRLF行结束符

算法复杂度分析

Endlessh的随机数生成算法具有恒定的时间复杂度:

  • rand16(): O(1)
  • randline(): O(n),其中n为行长度
  • 总体: O(1) 每行生成

这种高效的算法设计确保了Endlessh即使在处理大量并发连接时也能保持低资源消耗,这正是蜜罐系统所需要的特性。

通过精心设计的随机数生成和防探测算法,Endlessh成功实现了其作为SSH蜜罐的核心功能,既有效地拖延了攻击者,又保持了系统的轻量级和高性能特性。

总结

Endlessh的源码展示了C语言网络编程的精湛技艺,其架构设计兼顾了性能、稳定性和可维护性。通过事件驱动模型和poll()机制,单线程即可高效处理大量并发连接;精心设计的内存管理策略确保无内存泄漏;完善的信号处理机制支持优雅关闭和配置热重载;随机数生成算法既保证了性能又实现了防探测功能。这些最佳实践为网络服务开发提供了宝贵参考,特别是在资源受限环境下构建高性能、高可靠服务的解决方案。Endlessh不仅是功能完善的SSH蜜罐,更是C语言网络编程的教科书级范例。

【免费下载链接】endlessh SSH tarpit that slowly sends an endless banner 【免费下载链接】endlessh 项目地址: https://gitcode.com/gh_mirrors/en/endlessh

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值