如何在SOCKS5转发完成后释放accept_conn_cb中创建的bev?
我用Libevent实现了SOCKS5转发代理,但当客户端请求头不含Connection: close字段时,accept_conn_cb中创建的bev无法被释放。尝试修改event_cb同时释放bev和对应server bufferevent无效,目前通过设置5秒超时临时解决,求更优方案。
相关代码
核心实现代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/tcp.h> #include <event2/event.h> #include <event2/buffer.h> #include <event2/bufferevent.h> #include <event2/listener.h> #define BUFFER_SIZE 1024 #define SOCKS_ATYP_IPV6 4 #define RESPONSE_SIZE 10 #define SOCKS_VERSION 0x05 #define AUTH_METHOD_NO_AUTH 0x00 #define SOCKS_CMD_CONNECT 0x01 #define SOCKS_CMD_ASSOCIATE 0x03 #define SOCKS_ATYP_IPV4 0x01 #define SOCKS_ATYP_DOMAINNAME 0x03 void socks5_request(struct bufferevent *bev, void *ctx); void event_cb(struct bufferevent *bev, short events, void *ctx); void socks5_handshake(struct bufferevent *bev, void *ctx); void write_cb(struct bufferevent *bev, void *ctx); void forward_to_server(struct bufferevent *bev, void *ctx); void forward_to_client(struct bufferevent *bev, void *ctx); void accept_conn_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *address, int socklen, void *ctx); void accept_error_cb(struct evconnlistener *listener, void *ctx); void event_cb(struct bufferevent *bev, short events, void *ctx) { if (events & BEV_EVENT_ERROR) { perror("Error on bufferevent"); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { // Connection closed, free bufferevent printf("free bufferevent bev\n"); bufferevent_free(bev); } } // socks5 handshake void socks5_handshake(struct bufferevent *bev, void *ctx) { struct evbuffer *input = bufferevent_get_input(bev); size_t len = evbuffer_get_length(input); unsigned char buffer[2]; if (len < 2) { printf("need more data\n"); return; } evbuffer_copyout(input, buffer, 2); if (buffer[0] != SOCKS_VERSION) { bufferevent_free(bev); evbuffer_drain(input, evbuffer_get_length(input)); return; } // no auth unsigned char response[2] = {SOCKS_VERSION, AUTH_METHOD_NO_AUTH}; bufferevent_write(bev, response, 2); // clean input evbuffer_drain(input, evbuffer_get_length(input)); // handle socks5 request bufferevent_setcb(bev, socks5_request, NULL, event_cb, ctx); } // forward to server void forward_to_server(struct bufferevent *bev, void *ctx) { struct bufferevent *server = ctx; struct evbuffer *input = bufferevent_get_input(bev); struct evbuffer *output = bufferevent_get_output(server); evbuffer_add_buffer(output, input); } // forward to client void forward_to_client(struct bufferevent *bev, void *ctx) { struct bufferevent *client = ctx; struct evbuffer *input = bufferevent_get_input(bev); struct evbuffer *output = bufferevent_get_output(client); evbuffer_add_buffer(output, input); } // handle request void socks5_request(struct bufferevent *bev, void *ctx) { struct evbuffer *input = bufferevent_get_input(bev); size_t len = evbuffer_get_length(input); unsigned char atyp; unsigned char buffer[BUFFER_SIZE]; if (len < 5) { printf("need more data\n"); return; } evbuffer_copyout(input, buffer, sizeof(buffer)); if (buffer[1] != SOCKS_CMD_CONNECT) { printf("unsupported command %d\n", buffer[1]); bufferevent_free(bev); evbuffer_drain(input, len); return; } atyp = buffer[3]; // Address type (ATYP) if (atyp == SOCKS_ATYP_IPV4) { if (len < 10) return; unsigned char addr[4]; unsigned char port[2]; evbuffer_copyout(input, buffer, 4); // First 4 bytes of request evbuffer_drain(input, 4); evbuffer_remove(input, addr, 4); // Copy IPv4 address evbuffer_remove(input, port, 2); // Copy port char target_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, addr, target_ip, INET_ADDRSTRLEN); int target_port = (port[0] << 8) | port[1]; // connect dst struct event_base *base = bufferevent_get_base(bev); struct bufferevent *server = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE); bufferevent_socket_connect_hostname(server, NULL, AF_INET, target_ip, target_port); // response success unsigned char response[10] = {SOCKS_VERSION, 0x00, 0x00, SOCKS_ATYP_IPV4, 0, 0, 0, 0, 0, 0}; bufferevent_write(bev, response, 10); bufferevent_setcb(bev, forward_to_server, NULL, event_cb, server); bufferevent_setcb(server, forward_to_client, NULL, event_cb, bev); bufferevent_enable(bev, EV_READ | EV_WRITE); bufferevent_enable(server, EV_READ | EV_WRITE); } else if (atyp == SOCKS_ATYP_DOMAINNAME) { int domain_len = buffer[4]; char domain[256]; memcpy(domain, &buffer[5], domain_len); domain[domain_len] = '\0'; struct hostent *he = gethostbyname(domain); if (he == NULL) { printf("Failed to resolve domain: %s\n", domain); bufferevent_free(bev); evbuffer_drain(input, len); return; } struct sockaddr_in ipv4; memset(&ipv4, 0, sizeof(ipv4)); ipv4.sin_family = AF_INET; memcpy(&ipv4.sin_addr, he->h_addr, he->h_length); memcpy(&ipv4.sin_port, &buffer[5 + domain_len], 2); struct event_base *base = bufferevent_get_base(bev); struct bufferevent *server = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE); bufferevent_socket_connect(server, (struct sockaddr *)&ipv4, sizeof(struct sockaddr_in)); evbuffer_drain(input, 5 + domain_len + 2); // response success unsigned char response[10] = {SOCKS_VERSION, 0x00, 0x00, SOCKS_ATYP_IPV4, 0, 0, 0, 0, 0, 0}; bufferevent_write(bev, response, 10); bufferevent_setcb(bev, forward_to_server, NULL, event_cb, server); bufferevent_setcb(server, forward_to_client, NULL, event_cb, bev); bufferevent_enable(bev, EV_READ | EV_WRITE); bufferevent_enable(server, EV_READ | EV_WRITE); } else { bufferevent_free(bev); } evbuffer_drain(input, len); } // handle new accept void accept_conn_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *address, int socklen, void *ctx) { struct event_base *base = evconnlistener_get_base(listener); struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE); // set socks5_handshake bufferevent_setcb(bev, socks5_handshake, NULL, NULL, NULL); bufferevent_enable(bev, EV_READ | EV_WRITE); } // handle accept error void accept_error_cb(struct evconnlistener *listener, void *ctx) { struct event_base *base = evconnlistener_get_base(listener); int err = EVUTIL_SOCKET_ERROR(); fprintf(stderr, "Accept error %d (%s)\n", err, evutil_socket_error_to_string(err)); event_base_loopexit(base, NULL); } int main() { int port = 10802; struct event_base *base; struct evconnlistener *listener; struct sockaddr_in sin; // create base base = event_base_new(); if (!base) { return 1; } memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons(port); listener = evconnlistener_new_bind(base, accept_conn_cb, NULL, LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1, (struct sockaddr*)&sin, sizeof(sin)); if (!listener) { perror("evconnlistener_new_bind()"); return 1; } evconnlistener_set_error_cb(listener, accept_error_cb); event_base_dispatch(base); evconnlistener_free(listener); event_base_free(base); return 0; }
尝试过的无效修改(event_cb)
void event_cb(struct bufferevent *bev, short events, void *ctx) { struct bufferevent *client = ctx; if (events & BEV_EVENT_ERROR) { perror("Error on bufferevent"); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { // Connection closed, free bufferevent bufferevent_free(bev); if (client) { bufferevent_free(client); } } }
临时解决方法(超时)
在socks5_request中添加:
struct timeval read_timeout = {5, 0}; // 5 seconds struct timeval write_timeout = {5, 0}; // 5 seconds bufferevent_set_timeouts(bev, &read_timeout, &write_timeout); bufferevent_set_timeouts(server, &read_timeout, &write_timeout);
在event_cb中添加:
if (events & BEV_EVENT_TIMEOUT) { if (bev) { bufferevent_free(bev); } }
更优解决方案分析
问题根源在于:当客户端和服务端都使用长连接(不发送Connection: close)且没有数据传输时,Libevent不会触发BEV_EVENT_EOF或BEV_EVENT_ERROR,导致bufferevent一直被持有无法释放。
方案1:完善双向连接的关闭逻辑
之前修改event_cb无效的原因是,释放其中一个bufferevent时,另一个的ctx指针可能已失效或被重复释放。需要先解除两者的回调关联,避免野指针:
void event_cb(struct bufferevent *bev, short events, void *ctx) { struct bufferevent *peer_bev = ctx; if (events & BEV_EVENT_ERROR) { perror("Error on bufferevent"); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR | BEV_EVENT_TIMEOUT)) { // 先解除对方的回调关联,防止重复释放 if (peer_bev) { bufferevent_setcb(peer_bev, NULL, NULL, NULL, NULL); bufferevent_free(peer_bev); } bufferevent_free(bev); } }
方案2:启用TCP Keepalive
利用TCP层的Keepalive机制自动检测死连接,当连接断开时Libevent会触发关闭事件,从而释放bufferevent。在创建bufferevent后添加以下配置:
// 启用TCP Keepalive int keepalive = 1; int idle = 30; // 30秒无数据发送探测包 int interval = 5; // 探测包间隔5秒 int count = 3; // 3次探测无响应则关闭连接 setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive)); setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle)); setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval)); setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count));
注:fd为对应bufferevent的socket描述符,在accept_conn_cb中是传入的fd,在socks5_request中可通过bufferevent_getfd(server)获取。
方案3:主动检测连接空闲状态
在读写回调中记录连接最后活跃时间,通过定时事件遍历所有连接,释放超过阈值的空闲连接。这种方式比固定超时更灵活,可根据业务调整空闲时长。
推荐方案
优先选择方案1 + 方案2的组合:
- 完善event_cb的释放逻辑,确保双向bufferevent都能被正确释放,避免野指针问题。
- 启用TCP Keepalive,让系统自动检测死连接,触发关闭事件。
该组合既解决了长连接空闲时的资源释放问题,又能处理异常断开的情况,比单纯的超时方案更可靠。
内容的提问来源于stack exchange,提问作者Jacky Lam

