Просмотр исходного кода

feature: Introduce HTTP/2 server support

Nick Peng 3 недель назад
Родитель
Сommit
4576afa8f6

+ 1 - 0
src/dns.c

@@ -2236,6 +2236,7 @@ static int _dns_decode_opt(struct dns_context *context, dns_rr_type type, unsign
 		switch (opt_code) {
 		case DNS_OPT_T_ECS: {
 			struct dns_opt_ecs ecs;
+			memset(&ecs, 0, sizeof(ecs));
 			ret = _dns_decode_opt_ecs(context, &ecs, opt_len);
 			if (ret != 0) {
 				tlog(TLOG_ERROR, "decode ecs failed.");

+ 41 - 18
src/dns_client/client_http2.c

@@ -57,7 +57,9 @@ static int _dns_client_send_http2_stream(struct dns_server_info *server_info, st
 		return -1;
 	}
 
+	pthread_mutex_lock(&server_info->lock);
 	conn_stream->http2_stream = http2_stream;
+	pthread_mutex_unlock(&server_info->lock);
 	http2_stream_set_ex_data(http2_stream, conn_stream);
 
 	/* Set request headers */
@@ -68,15 +70,19 @@ static int _dns_client_send_http2_stream(struct dns_server_info *server_info, st
 										  {NULL, NULL}};
 
 	if (http2_stream_set_request(http2_stream, "POST", https_flag->path, headers) < 0) {
-		http2_stream_free(http2_stream);
+		pthread_mutex_lock(&server_info->lock);
 		conn_stream->http2_stream = NULL;
+		pthread_mutex_unlock(&server_info->lock);
+		http2_stream_put(http2_stream);
 		return -1;
 	}
 
 	/* Write request body */
 	if (http2_stream_write_body(http2_stream, (const uint8_t *)data, len, 1) < 0) {
-		http2_stream_free(http2_stream);
+		pthread_mutex_lock(&server_info->lock);
 		conn_stream->http2_stream = NULL;
+		pthread_mutex_unlock(&server_info->lock);
+		http2_stream_put(http2_stream);
 		return -1;
 	}
 
@@ -88,10 +94,11 @@ static void _dns_client_cleanup_http2_stream(struct dns_server_info *server_info
 											 struct http2_stream *http2_stream)
 {
 	pthread_mutex_lock(&server_info->lock);
-	http2_stream_free(http2_stream);
 	conn_stream->http2_stream = NULL;
 	list_del_init(&conn_stream->server_list);
 	pthread_mutex_unlock(&server_info->lock);
+
+	http2_stream_put(http2_stream);
 	_dns_client_conn_stream_put(conn_stream);
 }
 
@@ -142,21 +149,36 @@ static void _dns_client_send_buffered_http2_requests(struct dns_server_info *ser
 	struct dns_conn_stream *conn_stream = NULL;
 	struct dns_conn_stream *tmp = NULL;
 
-	pthread_mutex_lock(&server_info->lock);
-	list_for_each_entry_safe(conn_stream, tmp, &server_info->conn_stream_list, server_list)
-	{
-		if (conn_stream->http2_stream != NULL || conn_stream->send_buff.len <= 0) {
-			continue;
+	while (1) {
+		struct dns_conn_stream *target_stream = NULL;
+
+		pthread_mutex_lock(&server_info->lock);
+		list_for_each_entry_safe(conn_stream, tmp, &server_info->conn_stream_list, server_list)
+		{
+			if (conn_stream->http2_stream != NULL || conn_stream->send_buff.len <= 0) {
+				continue;
+			}
+			target_stream = conn_stream;
+			_dns_client_conn_stream_get(target_stream);
+			break;
+		}
+		pthread_mutex_unlock(&server_info->lock);
+
+		if (target_stream == NULL) {
+			break;
 		}
 
 		/* Send buffered request using helper function */
-		if (_dns_client_send_http2_stream(server_info, conn_stream, conn_stream->send_buff.data,
-										  conn_stream->send_buff.len) == 0) {
+		if (_dns_client_send_http2_stream(server_info, target_stream, target_stream->send_buff.data,
+										  target_stream->send_buff.len) == 0) {
 			/* Clear buffer as it's now in HTTP/2 stream buffer */
-			conn_stream->send_buff.len = 0;
+			target_stream->send_buff.len = 0;
+		} else {
+			_dns_client_release_stream_on_error(server_info, target_stream);
 		}
+
+		_dns_client_conn_stream_put(target_stream);
 	}
-	pthread_mutex_unlock(&server_info->lock);
 }
 
 int _dns_client_send_http2(struct dns_server_info *server_info, struct dns_query_struct *query, void *packet,
@@ -226,7 +248,7 @@ int _dns_client_send_http2(struct dns_server_info *server_info, struct dns_query
 
 	/* Initialize HTTP/2 context if not already done */
 	if (server_info->http2_ctx == NULL) {
-		http2_ctx = http2_ctx_client_new(https_flag->httphost, _http2_bio_read, _http2_bio_write, server_info);
+		http2_ctx = http2_ctx_client_new(https_flag->httphost, _http2_bio_read, _http2_bio_write, server_info, NULL);
 		if (http2_ctx == NULL) {
 			tlog(TLOG_ERROR, "init http2 context failed.");
 			goto errout;
@@ -281,7 +303,7 @@ errout:
 	_dns_client_release_stream_on_error(server_info, stream);
 
 	if (http2_stream) {
-		http2_stream_free(http2_stream);
+		http2_stream_put(http2_stream);
 	}
 	if (http2_ctx && server_info->http2_ctx == NULL) {
 		http2_ctx_free(http2_ctx);
@@ -297,13 +319,12 @@ int _dns_client_process_http2(struct dns_server_info *server_info, struct epoll_
 	/* Initialize context if needed (e.g. first time in EPOLLOUT) */
 	if (http2_ctx == NULL) {
 		struct client_dns_server_flag_https *https_flag = &server_info->flags.https;
-		http2_ctx = http2_ctx_client_new(https_flag->httphost, _http2_bio_read, _http2_bio_write, server_info);
+		http2_ctx = http2_ctx_client_new(https_flag->httphost, _http2_bio_read, _http2_bio_write, server_info, NULL);
 		if (http2_ctx == NULL) {
 			tlog(TLOG_ERROR, "init http2 context failed.");
 			goto errout;
 		}
 		server_info->http2_ctx = http2_ctx;
-		http2_ctx_handshake(http2_ctx);
 	}
 
 	/* Handle EPOLLOUT - flush pending writes and send buffered requests */
@@ -357,7 +378,9 @@ int _dns_client_process_http2(struct dns_server_info *server_info, struct epoll_
 			/* Poll for stream readiness */
 			ret = http2_ctx_poll(http2_ctx, poll_items, 10, &poll_count);
 			if (ret < 0) {
-				tlog(TLOG_DEBUG, "http2 poll failed, ret=%d", ret);
+				if (ret != HTTP2_ERR_EOF) {
+					tlog(TLOG_DEBUG, "http2 poll failed, ret=%d", ret);
+				}
 				goto errout;
 			}
 
@@ -379,7 +402,7 @@ int _dns_client_process_http2(struct dns_server_info *server_info, struct epoll_
 				conn_stream = (struct dns_conn_stream *)http2_stream_get_ex_data(http2_stream);
 				if (conn_stream == NULL) {
 					tlog(TLOG_DEBUG, "conn_stream is null for http2 stream");
-					http2_stream_free(http2_stream);
+					http2_stream_put(http2_stream);
 					continue;
 				}
 

+ 1 - 1
src/dns_client/client_socket.c

@@ -134,7 +134,7 @@ void _dns_client_close_socket_ext(struct dns_server_info *server_info, int no_de
 			list_for_each_entry_safe(conn_stream, tmp, &server_info->conn_stream_list, server_list)
 			{
 				if (conn_stream->http2_stream) {
-					http2_stream_free(conn_stream->http2_stream);
+					http2_stream_put(conn_stream->http2_stream);
 					conn_stream->http2_stream = NULL;
 				}
 

+ 2 - 2
src/dns_client/conn_stream.c

@@ -67,7 +67,7 @@ void _dns_client_conn_stream_put(struct dns_conn_stream *stream)
 	if (stream->http2_stream) {
 		struct http2_stream *http2_stream = stream->http2_stream;
 		stream->http2_stream = NULL;
-		http2_stream_free(http2_stream);
+		http2_stream_put(http2_stream);
 	}
 
 	if (stream->query) {
@@ -112,7 +112,7 @@ void _dns_client_conn_server_streams_free(struct dns_server_info *server_info, s
 		if (stream->http2_stream) {
 			struct http2_stream *http2_stream = stream->http2_stream;
 			stream->http2_stream = NULL;
-			http2_stream_free(http2_stream);
+			http2_stream_put(http2_stream);
 		}
 		_dns_client_conn_stream_put(stream);
 	}

+ 5 - 0
src/dns_conf/bind.c

@@ -215,6 +215,7 @@ static int _config_bind_ip(int argc, char *argv[], DNS_BIND_TYPE type)
 		{"force-https-soa", no_argument, NULL, 254},
 		{"ipset", required_argument, NULL, 255},
 		{"nftset", required_argument, NULL, 256},
+		{"alpn", required_argument, NULL, 257},
 		{NULL, no_argument, NULL, 0}
 	};
 	/* clang-format on */
@@ -342,6 +343,10 @@ static int _config_bind_ip(int argc, char *argv[], DNS_BIND_TYPE type)
 			server_flag |= BIND_FLAG_NO_SERVE_EXPIRED;
 			break;
 		}
+		case 257: {
+			safe_strncpy(bind_ip->alpn, optarg, DNS_MAX_ALPN_LEN);
+			break;
+		}
 		default:
 			if (optind > optind_last) {
 				tlog(TLOG_WARN, "unknown bind option: %s at '%s:%d'.", argv[optind - 1], conf_get_conf_file(),

+ 26 - 0
src/dns_server/connection.c

@@ -19,6 +19,8 @@
 #include "connection.h"
 #include "dns_server.h"
 
+#include "smartdns/http2.h"
+
 #include <openssl/ssl.h>
 #include <sys/epoll.h>
 #include <sys/eventfd.h>
@@ -66,6 +68,8 @@ void _dns_server_conn_release(struct dns_server_conn_head *conn)
 			SSL_CTX_free(tls_server->ssl_ctx);
 			tls_server->ssl_ctx = NULL;
 		}
+	} else if (conn->type == DNS_CONN_TYPE_HTTP2_STREAM) {
+		/* Nothing to release for stream connection wrapper, just free(conn) at the end */
 	}
 
 	if (conn->fd > 0) {
@@ -95,10 +99,23 @@ void _dns_server_close_socket(void)
 	struct dns_server_conn_head *conn = NULL;
 	struct dns_server_conn_head *tmp = NULL;
 
+	pthread_mutex_lock(&server.conn_list_lock);
 	list_for_each_entry_safe(conn, tmp, &server.conn_list, list)
 	{
+		/* Force cleanup of TLS/HTTPS client connections to prevent memory leaks */
+		if (conn->type == DNS_CONN_TYPE_TLS_CLIENT || conn->type == DNS_CONN_TYPE_HTTPS_CLIENT) {
+			struct dns_server_conn_tls_client *tls_client = (struct dns_server_conn_tls_client *)conn;
+			
+			/* Free SSL connection */
+			if (tls_client->ssl != NULL) {
+				SSL_free(tls_client->ssl);
+				tls_client->ssl = NULL;
+			}
+		}
+
 		_dns_server_client_close(conn);
 	}
+	pthread_mutex_unlock(&server.conn_list_lock);
 }
 
 void _dns_server_close_socket_server(void)
@@ -144,6 +161,15 @@ int _dns_server_client_close(struct dns_server_conn_head *conn)
 	list_del_init(&conn->list);
 	pthread_mutex_unlock(&server.conn_list_lock);
 
+	if (conn->type == DNS_CONN_TYPE_TLS_CLIENT || conn->type == DNS_CONN_TYPE_HTTPS_CLIENT) {
+		struct dns_server_conn_tls_client *tls_client = (struct dns_server_conn_tls_client *)conn;
+		if (tls_client->http2_ctx != NULL) {
+			http2_ctx_close(tls_client->http2_ctx);
+			http2_ctx_free(tls_client->http2_ctx);
+			tls_client->http2_ctx = NULL;
+		}
+	}
+
 	_dns_server_conn_release(conn);
 
 	return 0;

+ 3 - 0
src/dns_server/dns_server.c

@@ -43,6 +43,7 @@
 #include "server_tcp.h"
 #include "server_tls.h"
 #include "server_udp.h"
+#include "server_http2.h"
 #include "soa.h"
 #include "speed_check.h"
 
@@ -98,6 +99,8 @@ int _dns_reply_inpacket(struct dns_request *request, unsigned char *inpacket, in
 		ret = _dns_server_reply_tcp(request, (struct dns_server_conn_tcp_client *)conn, inpacket, inpacket_len);
 	} else if (conn->type == DNS_CONN_TYPE_HTTPS_CLIENT) {
 		ret = _dns_server_reply_https(request, (struct dns_server_conn_tcp_client *)conn, inpacket, inpacket_len);
+	} else if (conn->type == DNS_CONN_TYPE_HTTP2_STREAM) {
+		ret = _dns_server_reply_http2(request, (struct dns_server_conn_http2_stream *)conn, inpacket, inpacket_len);
 	} else {
 		ret = -1;
 	}

+ 4 - 0
src/dns_server/dns_server.h

@@ -26,6 +26,7 @@
 #include "smartdns/dns.h"
 #include "smartdns/dns_conf.h"
 #include "smartdns/dns_server.h"
+#include "smartdns/http2.h"
 #include "smartdns/tlog.h"
 #include "smartdns/util.h"
 
@@ -76,6 +77,7 @@ typedef enum {
 	DNS_CONN_TYPE_TLS_CLIENT,
 	DNS_CONN_TYPE_HTTPS_SERVER,
 	DNS_CONN_TYPE_HTTPS_CLIENT,
+	DNS_CONN_TYPE_HTTP2_STREAM,
 } DNS_CONN_TYPE;
 
 typedef enum DNS_CHILD_POST_RESULT {
@@ -212,6 +214,8 @@ struct dns_server_conn_tls_client {
 	SSL *ssl;
 	int ssl_want_write;
 	pthread_mutex_t ssl_lock;
+	void *http2_ctx;
+	char alpn_selected[32];
 };
 
 /* ip address lists of domain */

+ 315 - 0
src/dns_server/server_http2.c

@@ -0,0 +1,315 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2025 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ *************************************************************************/
+
+#include "server_http2.h"
+#include "connection.h"
+#include "dns_server.h"
+#include "server_tls.h"
+#include "smartdns/http2.h"
+#include "smartdns/tlog.h"
+#include "smartdns/util.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define DNS_SERVER_HTTP2_MAX_CONCURRENT_STREAMS 4096
+
+static int _http2_server_bio_read(void *private_data, uint8_t *buf, int len)
+{
+	struct dns_server_conn_tls_client *tls_client = (struct dns_server_conn_tls_client *)private_data;
+	return _dns_server_socket_ssl_recv(tls_client, buf, len);
+}
+
+static int _http2_server_bio_write(void *private_data, const uint8_t *buf, int len)
+{
+	struct dns_server_conn_tls_client *tls_client = (struct dns_server_conn_tls_client *)private_data;
+	return _dns_server_socket_ssl_send(tls_client, buf, len);
+}
+
+static int _dns_server_http2_send_response(struct http2_stream *stream, int status, const char *content_type,
+										   const void *body, int body_len)
+{
+	char content_length[32];
+	snprintf(content_length, sizeof(content_length), "%d", body_len);
+
+	struct http2_header_pair headers[] = {
+		{"content-type", content_type}, {"content-length", content_length}, {NULL, NULL}};
+
+	if (http2_stream_set_response(stream, status, headers, 2) < 0) {
+		return -1;
+	}
+
+	if (http2_stream_write_body(stream, (const uint8_t *)body, body_len, 1) < 0) {
+		return -1;
+	}
+
+	return 0;
+}
+
+int _dns_server_reply_http2(struct dns_request *request, struct dns_server_conn_http2_stream *stream_conn,
+							unsigned char *inpacket, int inpacket_len)
+{
+	struct http2_stream *stream = stream_conn->stream;
+
+	if (stream == NULL) {
+		return -1;
+	}
+
+	/* Send DNS response */
+	/* Content-Type for DoH is application/dns-message */
+	return _dns_server_http2_send_response(stream, 200, "application/dns-message", inpacket, inpacket_len);
+}
+
+static void _dns_server_http2_process_stream(struct dns_server_conn_tls_client *tls_client, struct http2_stream *stream)
+{
+	uint8_t buf[DNS_IN_PACKSIZE];
+	int len = 0;
+
+	const char *method = http2_stream_get_method(stream);
+	if (method == NULL) {
+		return;
+	}
+
+	if (strcasecmp(method, "POST") == 0) {
+		/* Read request body */
+		len = http2_stream_read_body(stream, buf, sizeof(buf));
+		if (len < 0) {
+			/* Error or no data yet */
+			return;
+		}
+
+		if (len == 0 && !http2_stream_is_end(stream)) {
+			/* No data available but stream not ended */
+			return;
+		}
+	} else if (strcasecmp(method, "GET") == 0) {
+		const char *path = http2_stream_get_path(stream);
+		char *base64_query = NULL;
+
+		if (http2_stream_get_ex_data(stream)) {
+			return;
+		}
+		http2_stream_set_ex_data(stream, (void *)1);
+
+		/* Consume any body (should be empty for GET) to mark stream as read-handled */
+		http2_stream_read_body(stream, NULL, 0);
+
+		if (path == NULL) {
+			_dns_server_http2_send_response(stream, 404, "text/plain", "Not Found", 9);
+			return;
+		}
+
+		/* Check path prefix */
+		if (strncmp(path, "/dns-query", 10) != 0) {
+			_dns_server_http2_send_response(stream, 404, "text/plain", "Not Found", 9);
+			return;
+		}
+
+		/* Parse query string */
+		char *query_val = http2_stream_get_query_param(stream, "dns");
+		if (query_val == NULL) {
+			_dns_server_http2_send_response(stream, 400, "text/plain", "Bad Request", 11);
+			return;
+		}
+
+		base64_query = malloc(DNS_IN_PACKSIZE);
+		if (base64_query == NULL) {
+			free(query_val);
+			return;
+		}
+
+		if (urldecode(base64_query, DNS_IN_PACKSIZE, query_val) < 0) {
+			free(query_val);
+			free(base64_query);
+			_dns_server_http2_send_response(stream, 400, "text/plain", "Bad Request", 11);
+			return;
+		}
+		free(query_val);
+
+		len = SSL_base64_decode_ext(base64_query, buf, sizeof(buf), 1, 1);
+		free(base64_query);
+
+		if (len <= 0) {
+			_dns_server_http2_send_response(stream, 400, "text/plain", "Bad Request", 11);
+			return;
+		}
+	} else {
+		_dns_server_http2_send_response(stream, 405, "text/plain", "Method Not Allowed", 18);
+		return;
+	}
+
+	if (len > 0) {
+		/* Create a fake connection object for this stream */
+		struct dns_server_conn_http2_stream *stream_conn = zalloc(1, sizeof(struct dns_server_conn_http2_stream));
+		if (stream_conn == NULL) {
+			tlog(TLOG_ERROR, "malloc failed for stream conn");
+			return;
+		}
+
+		/* Initialize the fake connection */
+		_dns_server_conn_head_init(&stream_conn->head, -1, DNS_CONN_TYPE_HTTP2_STREAM);
+		stream_conn->stream = stream;
+		stream_conn->tls_client = tls_client;
+
+		/* Copy properties from parent connection */
+		stream_conn->head.server_flags = tls_client->tcp.head.server_flags;
+		stream_conn->head.dns_group = tls_client->tcp.head.dns_group;
+		stream_conn->head.ipset_nftset_rule = tls_client->tcp.head.ipset_nftset_rule;
+
+		/* We need to increment refcnt because _dns_server_recv (via request) will eventually release it */
+		_dns_server_conn_get(&stream_conn->head);
+
+		/* Process the packet */
+		/* Note: _dns_server_recv takes conn, inpacket, inpacket_len, local, local_len, from, from_len */
+		_dns_server_recv(&stream_conn->head, buf, len, &tls_client->tcp.localaddr, tls_client->tcp.localaddr_len,
+						 &tls_client->tcp.addr, tls_client->tcp.addr_len);
+
+		/* Release our reference (request holds one now) */
+		_dns_server_conn_release(&stream_conn->head);
+	}
+}
+
+int _dns_server_process_http2(struct dns_server_conn_tls_client *tls_client, struct epoll_event *event,
+							  unsigned long now)
+{
+	struct http2_ctx *ctx = (struct http2_ctx *)tls_client->http2_ctx;
+	int ret = 0;
+
+	/* Initialize HTTP/2 context if not already done */
+	if (ctx == NULL) {
+		struct http2_settings settings;
+		memset(&settings, 0, sizeof(settings));
+		settings.max_concurrent_streams = DNS_SERVER_HTTP2_MAX_CONCURRENT_STREAMS;
+		ctx = http2_ctx_server_new("smartdns-server", _http2_server_bio_read, _http2_server_bio_write, tls_client, &settings);
+		if (ctx == NULL) {
+			tlog(TLOG_ERROR, "init http2 context failed.");
+			return -1;
+		}
+		tls_client->http2_ctx = ctx;
+
+		/* Perform initial handshake */
+		ret = http2_ctx_handshake(ctx);
+		if (ret < 0) {
+			const char *err_msg = http2_error_to_string(ret);
+			int log_level = TLOG_ERROR;
+			if (ret == HTTP2_ERR_EOF || ret == HTTP2_ERR_HTTP1) {
+				log_level = TLOG_DEBUG; /* Less noisy for clients that disconnect early or misbehave */
+			}
+			tlog(log_level, "http2 handshake failed, ret=%d (%s), alpn=%s.", ret, err_msg, tls_client->alpn_selected);
+			return -1;
+		}
+	}
+
+	/* Handle EPOLLOUT - flush pending writes */
+	if (event->events & EPOLLOUT) {
+		struct http2_poll_item poll_items[1];
+		int poll_count = 0;
+		int loop = 0;
+		while (http2_ctx_want_write(ctx) && loop++ < 10) {
+			ret = http2_ctx_poll(ctx, poll_items, 1, &poll_count);
+			if (ret < 0) {
+				break;
+			}
+		}
+	}
+
+	/* Handle EPOLLIN - read and process data */
+	if (event->events & EPOLLIN) {
+		struct http2_poll_item poll_items[10];
+		int poll_count = 0;
+		int loop_count = 0;
+		const int MAX_LOOP_COUNT = 128;
+
+		/* Ensure handshake is complete */
+		ret = http2_ctx_handshake(ctx);
+		if (ret < 0) {
+			const char *err_msg = http2_error_to_string(ret);
+			int log_level = TLOG_ERROR;
+			if (ret == HTTP2_ERR_EOF || ret == HTTP2_ERR_HTTP1) {
+				log_level = TLOG_DEBUG; /* Less noisy for clients that disconnect early or misbehave */
+			}
+			tlog(log_level, "http2 handshake failed, ret=%d (%s), alpn=%s.", ret, err_msg, tls_client->alpn_selected);
+			return -1;
+		} else if (ret == 0) {
+			/* Handshake in progress */
+			goto update_epoll;
+		}
+
+		/* Poll and process */
+		while (loop_count++ < MAX_LOOP_COUNT) {
+			poll_count = 0;
+			ret = http2_ctx_poll(ctx, poll_items, 10, &poll_count);
+			if (ret < 0) {
+				if (ret == HTTP2_ERR_EAGAIN) {
+					break;
+				}
+				if (ret == HTTP2_ERR_EOF) {
+					/* Connection closed by peer */
+					_dns_server_client_close(&tls_client->tcp.head);
+					return 0;
+				}
+				tlog(TLOG_DEBUG, "http2 poll failed, %s", http2_error_to_string(ret));
+				return -1;
+			}
+
+			if (poll_count == 0) {
+				continue;
+			}
+
+			for (int i = 0; i < poll_count; i++) {
+				if (poll_items[i].stream == NULL) {
+					if (poll_items[i].readable) {
+						struct http2_stream *stream = http2_ctx_accept_stream(ctx);
+						if (stream) {
+							_dns_server_http2_process_stream(tls_client, stream);
+						}
+					}
+					continue;
+				}
+
+				if (poll_items[i].stream && poll_items[i].readable) {
+					_dns_server_http2_process_stream(tls_client, poll_items[i].stream);
+				}
+			}
+		}
+	}
+
+update_epoll:
+	/* Update epoll events */
+	{
+		int epoll_events = EPOLLIN;
+		if (http2_ctx_want_write(ctx)) {
+			epoll_events |= EPOLLOUT;
+		}
+
+		struct epoll_event mod_event;
+		memset(&mod_event, 0, sizeof(mod_event));
+		mod_event.events = epoll_events;
+		mod_event.data.ptr = tls_client;
+
+		if (epoll_ctl(server.epoll_fd, EPOLL_CTL_MOD, tls_client->tcp.head.fd, &mod_event) != 0) {
+			tlog(TLOG_ERROR, "epoll ctl failed, %s", strerror(errno));
+			return -1;
+		}
+	}
+
+	return 0;
+}

+ 47 - 0
src/dns_server/server_http2.h

@@ -0,0 +1,47 @@
+/*************************************************************************
+ *
+ * Copyright (C) 2018-2025 Ruilin Peng (Nick) <[email protected]>.
+ *
+ * smartdns is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * smartdns is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ *************************************************************************/
+
+#ifndef _SERVER_HTTP2_H_
+#define _SERVER_HTTP2_H_
+
+#include "dns_server.h"
+#include "smartdns/http2.h"
+#include <sys/epoll.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct dns_server_conn_http2_stream {
+	struct dns_server_conn_head head;
+	struct http2_stream *stream;
+	struct dns_server_conn_tls_client *tls_client;
+};
+
+int _dns_server_process_http2(struct dns_server_conn_tls_client *tls_client, struct epoll_event *event,
+							  unsigned long now);
+
+int _dns_server_reply_http2(struct dns_request *request, struct dns_server_conn_http2_stream *stream_conn,
+							unsigned char *inpacket, int inpacket_len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif

+ 4 - 1
src/dns_server/server_https.c

@@ -21,6 +21,9 @@
 #include "dns_server.h"
 #include "server_socket.h"
 #include "server_tcp.h"
+#include "server_tls.h"
+
+#include "smartdns/http2.h"
 
 #include <errno.h>
 #include <string.h>
@@ -95,4 +98,4 @@ int _dns_server_reply_https(struct dns_request *request, struct dns_server_conn_
 	}
 
 	return 0;
-}
+}

+ 2 - 0
src/dns_server/server_https.h

@@ -20,6 +20,7 @@
 #define _DNS_SERVER_HTTPS_
 
 #include "dns_server.h"
+#include <sys/epoll.h>
 
 #ifdef __cplusplus
 extern "C" {
@@ -30,6 +31,7 @@ int _dns_server_reply_http_error(struct dns_server_conn_tcp_client *tcpclient, i
 
 int _dns_server_reply_https(struct dns_request *request, struct dns_server_conn_tcp_client *tcpclient, void *packet,
 							unsigned short len);
+
 #ifdef __cplusplus
 }
 #endif /*__cplusplus */

+ 71 - 1
src/dns_server/server_tls.c

@@ -20,8 +20,12 @@
 #include "server_tls.h"
 #include "connection.h"
 #include "dns_server.h"
+#include "server_https.h"
 #include "server_socket.h"
 #include "server_tcp.h"
+#include "server_http2.h"
+
+#include "smartdns/http2.h"
 
 #include <errno.h>
 #include <netinet/tcp.h>
@@ -31,6 +35,44 @@
 #include <string.h>
 #include <sys/epoll.h>
 
+static int alpn_select_cb(SSL *ssl, const unsigned char **out, unsigned char *outlen, const unsigned char *in,
+						  unsigned int inlen, void *arg)
+{
+	struct dns_bind_ip *bind_ip = (struct dns_bind_ip *)arg;
+	const char *alpn = bind_ip->alpn;
+	if (alpn[0] == '\0') {
+		alpn = "h2,http/1.1";
+	}
+
+	/* Parse server ALPN list */
+	char alpn_copy[256];
+	safe_strncpy(alpn_copy, alpn, sizeof(alpn_copy));
+	char *saveptr = NULL;
+	char *proto = strtok_r(alpn_copy, ",", &saveptr);
+
+	while (proto) {
+		unsigned int proto_len = strlen(proto);
+		for (unsigned int i = 0; i < inlen;) {
+			unsigned int len = in[i++];
+			if (i + len > inlen) {
+				break;
+			}
+
+			if (len == proto_len && memcmp(&in[i], proto, len) == 0) {
+				*out = &in[i];
+				*outlen = len;
+				return SSL_TLSEXT_ERR_OK;
+			}
+			i += len;
+		}
+		proto = strtok_r(NULL, ",", &saveptr);
+	}
+
+	/* No match found */
+	tlog(TLOG_DEBUG, "ALPN negotiation failed: no matching protocol found");
+	return SSL_TLSEXT_ERR_NOACK;
+}
+
 static ssize_t _ssl_read(struct dns_server_conn_tls_client *conn, void *buff, int num)
 {
 	ssize_t ret = 0;
@@ -137,7 +179,11 @@ int _dns_server_socket_ssl_send(struct dns_server_conn_tls_client *tls_client, c
 		ret = -1;
 		break;
 	case SSL_ERROR_SYSCALL:
-		tlog(TLOG_DEBUG, "SSL syscall failed, %s", strerror(errno));
+		if (errno == 0) {
+			tlog(TLOG_DEBUG, "SSL connection closed");
+		} else {
+			tlog(TLOG_DEBUG, "SSL syscall failed, %s", strerror(errno));
+		}
 		return ret;
 	default:
 		errno = EFAULT;
@@ -367,6 +413,18 @@ int _dns_server_process_tls(struct dns_server_conn_tls_client *tls_client, struc
 		}
 
 		tls_client->tcp.status = DNS_SERVER_CLIENT_STATUS_CONNECTED;
+
+		/* Get negotiated ALPN */
+		const unsigned char *alpn_data = NULL;
+		unsigned int alpn_len = 0;
+		SSL_get0_alpn_selected(tls_client->ssl, &alpn_data, &alpn_len);
+		if (alpn_data && alpn_len > 0 && alpn_len < sizeof(tls_client->alpn_selected)) {
+			memcpy(tls_client->alpn_selected, alpn_data, alpn_len);
+			tls_client->alpn_selected[alpn_len] = '\0';
+		} else {
+			safe_strncpy(tls_client->alpn_selected, "http/1.1", sizeof(tls_client->alpn_selected));
+		}
+
 		memset(&fd_event, 0, sizeof(fd_event));
 		fd_event.events = EPOLLIN | EPOLLOUT;
 		fd_event.data.ptr = tls_client;
@@ -376,6 +434,15 @@ int _dns_server_process_tls(struct dns_server_conn_tls_client *tls_client, struc
 		}
 	}
 
+	/* if HTTP/2 was negotiated */
+	if (strcmp(tls_client->alpn_selected, "h2") == 0) {
+		ret = _dns_server_process_http2(tls_client, event, now);
+		if (ret != 0) {
+			goto errout;
+		}
+		return ret;
+	}
+
 	return _dns_server_process_tcp((struct dns_server_conn_tcp_client *)tls_client, event, now);
 errout:
 	_dns_server_client_close(&tls_client->tcp.head);
@@ -456,6 +523,9 @@ int _dns_server_socket_tls(struct dns_bind_ip *bind_ip, DNS_CONN_TYPE conn_type)
 		goto errout;
 	}
 
+	/* Set ALPN */
+	SSL_CTX_set_alpn_select_cb(ssl_ctx, alpn_select_cb, bind_ip);
+
 	conn = zalloc(1, sizeof(struct dns_server_conn_tls_server));
 	if (conn == NULL) {
 		goto errout;

+ 169 - 25
src/http_parse/http2.c

@@ -17,6 +17,7 @@
  */
 
 #include "smartdns/http2.h"
+#include "smartdns/util.h"
 
 #include "hpack.h"
 #include "http_parse.h"
@@ -30,6 +31,26 @@
 #include <zlib.h>
 #endif
 
+const char *http2_error_to_string(int ret)
+{
+	switch (ret) {
+	case HTTP2_ERR_NONE:
+		return "no error";
+	case HTTP2_ERR_EAGAIN:
+		return "operation would block";
+	case HTTP2_ERR_EOF:
+		return "connection closed by client";
+	case HTTP2_ERR_IO:
+		return "I/O error";
+	case HTTP2_ERR_PROTOCOL:
+		return "protocol error";
+	case HTTP2_ERR_HTTP1:
+		return "client sent HTTP/1.1 after ALPN h2";
+	default:
+		return "unknown error";
+	}
+}
+
 /* HTTP/2 Frame Types */
 #define HTTP2_FRAME_DATA 0x00
 #define HTTP2_FRAME_HEADERS 0x01
@@ -91,6 +112,7 @@ struct http2_stream {
 	int body_read_offset;
 	int end_stream_received;
 	int end_stream_sent;
+	int end_stream_read_handled; /* Flag to track if EOF has been reported to app */
 	int window_size;
 	int body_decompressed; /* Flag to track if body has been decompressed */
 	void *ex_data;
@@ -116,6 +138,8 @@ struct http2_ctx {
 	int connection_window_size;
 	int peer_max_frame_size;
 	int peer_initial_window_size;
+	int active_streams;
+	struct http2_settings settings; /* HTTP/2 settings */
 
 	/* I/O state */
 	int want_read;
@@ -174,8 +198,6 @@ static void write_uint24(uint8_t *data, uint32_t value)
 	data[2] = value & 0xFF;
 }
 
-/* HPACK integer encoding/decoding */
-
 /* HPACK callback */
 static int http2_on_header(void *ctx, const char *name, const char *value)
 {
@@ -268,6 +290,7 @@ static int http2_write_frame_header(uint8_t *buf, int length, uint8_t type, uint
 static int http2_send_frame(struct http2_ctx *ctx, const uint8_t *data, int len)
 {
 	int total_sent = 0;
+	int unsent = 0;
 
 	/* First, try to flush any pending writes */
 	if (ctx->pending_write_len > 0) {
@@ -324,7 +347,7 @@ static int http2_send_frame(struct http2_ctx *ctx, const uint8_t *data, int len)
 
 buffer_new_data:
 	/* Buffer the unsent data */
-	int unsent = len - total_sent;
+	unsent = len - total_sent;
 	if (unsent > 0) {
 		/* Ensure buffer capacity */
 		int needed = ctx->pending_write_len + unsent;
@@ -352,7 +375,7 @@ buffer_new_data:
 
 static int http2_send_settings(struct http2_ctx *ctx, int ack)
 {
-	uint8_t frame[HTTP2_FRAME_HEADER_SIZE + 24]; /* Increased size for ENABLE_PUSH */
+	uint8_t frame[HTTP2_FRAME_HEADER_SIZE + 256]; /* Increased size for ENABLE_PUSH */
 	int offset = HTTP2_FRAME_HEADER_SIZE;
 	uint8_t flags = ack ? HTTP2_FLAG_ACK : 0;
 
@@ -378,6 +401,13 @@ static int http2_send_settings(struct http2_ctx *ctx, int ack)
 		write_uint32(frame + offset, (HTTP2_SETTINGS_MAX_FRAME_SIZE << 16) | 0);
 		write_uint32(frame + offset + 2, HTTP2_DEFAULT_MAX_FRAME_SIZE);
 		offset += 6;
+
+		if (ctx->settings.max_concurrent_streams > 0) {
+			/* SETTINGS_MAX_CONCURRENT_STREAMS */
+			write_uint32(frame + offset, (HTTP2_SETTINGS_MAX_CONCURRENT_STREAMS << 16) | 0);
+			write_uint32(frame + offset + 2, ctx->settings.max_concurrent_streams);
+			offset += 6;
+		}
 	}
 
 	http2_write_frame_header(frame, offset - HTTP2_FRAME_HEADER_SIZE, HTTP2_FRAME_SETTINGS, flags, 0);
@@ -467,6 +497,11 @@ static struct http2_stream *http2_find_stream(struct http2_ctx *ctx, int stream_
 
 static struct http2_stream *http2_create_stream(struct http2_ctx *ctx, int stream_id)
 {
+	/* Check concurrent streams limit */
+	if (ctx->active_streams >= ctx->settings.max_concurrent_streams && ctx->settings.max_concurrent_streams > 0) {
+		return NULL;
+	}
+
 	struct http2_stream *stream = malloc(sizeof(*stream));
 	if (!stream) {
 		return NULL;
@@ -491,8 +526,8 @@ static struct http2_stream *http2_create_stream(struct http2_ctx *ctx, int strea
 
 	stream->next = ctx->streams;
 	ctx->streams = stream;
+	ctx->active_streams++;
 
-	/* Increase ctx reference count */
 	http2_ctx_ref(ctx);
 
 	return stream;
@@ -504,6 +539,7 @@ static void http2_remove_stream(struct http2_ctx *ctx, struct http2_stream *stre
 	while (*p) {
 		if (*p == stream) {
 			*p = stream->next;
+			ctx->active_streams--;
 			return;
 		}
 		p = &(*p)->next;
@@ -708,6 +744,15 @@ static int http2_verify_connection_preface(struct http2_ctx *ctx)
 
 	/* Verify preface */
 	if (memcmp(ctx->read_buffer, HTTP2_CONNECTION_PREFACE, HTTP2_CONNECTION_PREFACE_LEN) != 0) {
+		/* Check if it looks like HTTP/1.1 */
+		if (ctx->read_buffer_len >= 4 &&
+			(memcmp(ctx->read_buffer, "GET ", 4) == 0 || memcmp(ctx->read_buffer, "POST ", 5) == 0 ||
+			 memcmp(ctx->read_buffer, "HEAD ", 5) == 0 || memcmp(ctx->read_buffer, "PUT ", 4) == 0 ||
+			 memcmp(ctx->read_buffer, "DELETE ", 7) == 0 || memcmp(ctx->read_buffer, "OPTIONS ", 8) == 0 ||
+			 memcmp(ctx->read_buffer, "PATCH ", 6) == 0)) {
+			ctx->status = HTTP2_ERR_HTTP1;
+			return HTTP2_ERR_HTTP1;
+		}
 		ctx->status = HTTP2_ERR_PROTOCOL;
 		return HTTP2_ERR_PROTOCOL;
 	}
@@ -866,7 +911,7 @@ void http2_ctx_unref(struct http2_ctx *ctx)
 	/* Reference count reached zero, free the context */
 	pthread_mutex_lock(&ctx->mutex);
 
-	/* Free all streams - each stream will call http2_stream_unref */
+	/* Free all streams - each stream will call http2_stream_put */
 	while (ctx->streams) {
 		struct http2_stream *next = ctx->streams->next;
 		ctx->streams->next = NULL; /* Detach from list */
@@ -893,7 +938,45 @@ void http2_ctx_unref(struct http2_ctx *ctx)
 	free(ctx);
 }
 
-struct http2_stream *http2_stream_ref(struct http2_stream *stream)
+void http2_ctx_close(struct http2_ctx *ctx)
+{
+	struct http2_stream *streams_to_free = NULL;
+
+	if (!ctx) {
+		return;
+	}
+
+	pthread_mutex_lock(&ctx->mutex);
+
+	/* Detach all streams from context */
+	streams_to_free = ctx->streams;
+	ctx->streams = NULL;
+
+	pthread_mutex_unlock(&ctx->mutex);
+
+	/* Now free streams outside the lock */
+	while (streams_to_free) {
+		struct http2_stream *stream = streams_to_free;
+		streams_to_free = stream->next;
+
+		/* We need to simulate stream destruction */
+		/* http2_stream_put expects stream to have refcount.
+		   But here we just want to break the cycle.
+		   The stream holds a reference to ctx.
+		   If we free the stream, we should release that reference. */
+
+		/* Manually free stream resources */
+		http2_free_headers(stream);
+		free(stream->body_buffer);
+
+		free(stream);
+
+		/* Release the reference to ctx that was taken when the stream was created */
+		http2_ctx_unref(ctx);
+	}
+}
+
+struct http2_stream *http2_stream_get(struct http2_stream *stream)
 {
 	if (!stream) {
 		return NULL;
@@ -902,7 +985,7 @@ struct http2_stream *http2_stream_ref(struct http2_stream *stream)
 	return stream;
 }
 
-void http2_stream_unref(struct http2_stream *stream)
+void http2_stream_put(struct http2_stream *stream)
 {
 	if (!stream) {
 		return;
@@ -931,14 +1014,13 @@ void http2_stream_unref(struct http2_stream *stream)
 /* Public API implementation */
 
 struct http2_ctx *http2_ctx_client_new(const char *server, http2_bio_read_fn bio_read, http2_bio_write_fn bio_write,
-									   void *private_data)
+									   void *private_data, const struct http2_settings *settings)
 {
-	struct http2_ctx *ctx = malloc(sizeof(*ctx));
+	struct http2_ctx *ctx = zalloc(1, sizeof(*ctx));
 	if (!ctx) {
 		return NULL;
 	}
 
-	memset(ctx, 0, sizeof(*ctx));
 	pthread_mutex_init(&ctx->mutex, NULL);
 	ctx->refcount = 1; /* Initial reference count */
 	ctx->is_client = 1;
@@ -950,6 +1032,12 @@ struct http2_ctx *http2_ctx_client_new(const char *server, http2_bio_read_fn bio
 	ctx->connection_window_size = HTTP2_DEFAULT_WINDOW_SIZE;
 	ctx->peer_max_frame_size = HTTP2_DEFAULT_MAX_FRAME_SIZE;
 	ctx->peer_initial_window_size = HTTP2_DEFAULT_WINDOW_SIZE;
+	ctx->active_streams = 0;
+
+	/* Initialize settings with defaults or provided values */
+	if (settings) {
+		ctx->settings = *settings;
+	}
 
 	/* Initialize I/O state */
 	ctx->want_read = 0;
@@ -971,14 +1059,13 @@ struct http2_ctx *http2_ctx_client_new(const char *server, http2_bio_read_fn bio
 }
 
 struct http2_ctx *http2_ctx_server_new(const char *server, http2_bio_read_fn bio_read, http2_bio_write_fn bio_write,
-									   void *private_data)
+									   void *private_data, const struct http2_settings *settings)
 {
-	struct http2_ctx *ctx = malloc(sizeof(*ctx));
+	struct http2_ctx *ctx = zalloc(1, sizeof(*ctx));
 	if (!ctx) {
 		return NULL;
 	}
 
-	memset(ctx, 0, sizeof(*ctx));
 	pthread_mutex_init(&ctx->mutex, NULL);
 	ctx->refcount = 1; /* Initial reference count */
 	ctx->is_client = 0;
@@ -990,6 +1077,12 @@ struct http2_ctx *http2_ctx_server_new(const char *server, http2_bio_read_fn bio
 	ctx->connection_window_size = HTTP2_DEFAULT_WINDOW_SIZE;
 	ctx->peer_max_frame_size = HTTP2_DEFAULT_MAX_FRAME_SIZE;
 	ctx->peer_initial_window_size = HTTP2_DEFAULT_WINDOW_SIZE;
+	ctx->active_streams = 0;
+
+	/* Initialize settings with defaults or provided values */
+	if (settings) {
+		ctx->settings = *settings;
+	}
 
 	/* Initialize I/O state */
 	ctx->want_read = 0;
@@ -1104,7 +1197,8 @@ int http2_ctx_poll(struct http2_ctx *ctx, struct http2_poll_item *items, int max
 		while (stream) {
 			/* Server accepts odd stream IDs (client-initiated) */
 			/* Stream is ready to accept when it has received complete request (END_STREAM) */
-			if ((stream->stream_id % 2) == 1 && !list_empty(&stream->header_list.list) && stream->end_stream_received) {
+			if ((stream->stream_id % 2) == 1 && !list_empty(&stream->header_list.list) && stream->end_stream_received &&
+				!stream->end_stream_sent) {
 				has_new_stream = 1;
 				break;
 			}
@@ -1129,7 +1223,7 @@ int http2_ctx_poll(struct http2_ctx *ctx, struct http2_poll_item *items, int max
 		 * 2. Stream has ended (all data including headers received)
 		 */
 		int has_body_data = stream->body_buffer_len > stream->body_read_offset;
-		int stream_ended = stream->end_stream_received;
+		int stream_ended = stream->end_stream_received && !stream->end_stream_read_handled;
 
 		int readable = has_body_data || stream_ended;
 		int writable = stream->state == HTTP2_STREAM_OPEN || stream->state == HTTP2_STREAM_HALF_CLOSED_REMOTE;
@@ -1149,15 +1243,15 @@ int http2_ctx_poll(struct http2_ctx *ctx, struct http2_poll_item *items, int max
 
 	/* If we found items, return success (0) even if there was an error/EOF.
 	   The error will be returned on the next call when no items are ready. */
-	if (count > 0) {
-		return 0;
-	}
-
 	/* If no items and we have an error/EOF, return it */
 	if (ret < 0 && ret != HTTP2_ERR_EAGAIN) {
 		return ret;
 	}
 
+	if (count > 0) {
+		return 0;
+	}
+
 	if (ctx->status < 0) {
 		return ctx->status;
 	}
@@ -1178,11 +1272,7 @@ struct http2_stream *http2_stream_new(struct http2_ctx *ctx)
 	return stream;
 }
 
-void http2_stream_free(struct http2_stream *stream)
-{
-	/* For backward compatibility, just call unref */
-	http2_stream_unref(stream);
-}
+
 
 int http2_stream_get_id(struct http2_stream *stream)
 {
@@ -1527,6 +1617,7 @@ int http2_stream_read_body(struct http2_stream *stream, uint8_t *data, int len)
 
 		/* If stream ended or connection has error, return 0 (EOF) */
 		if (stream->end_stream_received || ctx->status < 0) {
+			stream->end_stream_read_handled = 1;
 			return 0;
 		}
 
@@ -1649,3 +1740,56 @@ int http2_ctx_want_write(struct http2_ctx *ctx)
 
 	return want_write;
 }
+
+int http2_ctx_is_closed(struct http2_ctx *ctx)
+{
+	if (!ctx) {
+		return 1;
+	}
+
+	pthread_mutex_lock(&ctx->mutex);
+	int is_closed = (ctx->status < 0);
+	pthread_mutex_unlock(&ctx->mutex);
+
+	return is_closed;
+}
+
+char *http2_stream_get_query_param(struct http2_stream *stream, const char *name)
+{
+	const char *path = http2_stream_get_path(stream);
+	const char *q = NULL;
+	const char *val_start = NULL;
+	int name_len = 0;
+
+	if (path == NULL || name == NULL) {
+		return NULL;
+	}
+
+	q = strstr(path, "?");
+	if (q == NULL) {
+		return NULL;
+	}
+	q++;
+
+	name_len = strlen(name);
+
+	while (*q) {
+		if (strncmp(q, name, name_len) == 0 && q[name_len] == '=') {
+			val_start = q + name_len + 1;
+			break;
+		}
+		q = strchr(q, '&');
+		if (q == NULL) {
+			break;
+		}
+		q++;
+	}
+
+	if (val_start) {
+		const char *end = strchr(val_start, '&');
+		size_t val_len = end ? (size_t)(end - val_start) : strlen(val_start);
+		return strndup(val_start, val_len);
+	}
+
+	return NULL;
+}

+ 2 - 1
src/include/smartdns/dns_conf.h

@@ -565,6 +565,7 @@ struct dns_bind_ip {
 	const char *ssl_cert_key_pass;
 	const char *group;
 	struct nftset_ipset_rules nftset_ipset_rule;
+	char alpn[DNS_MAX_ALPN_LEN];
 };
 
 struct dns_domain_set_rule {
@@ -696,7 +697,7 @@ struct dns_config {
 	char bind_ca_key_file[DNS_MAX_PATH];
 	char bind_root_ca_key_file[DNS_MAX_PATH];
 	char bind_ca_key_pass[DNS_MAX_PATH];
-	int  bind_ca_validity_days;
+	int bind_ca_validity_days;
 	char need_cert;
 	int tcp_idle_time;
 	ssize_t cachesize;

+ 41 - 8
src/include/smartdns/http2.h

@@ -16,8 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef _HTTP2_CLIENT_H_
-#define _HTTP2_CLIENT_H_
+#ifndef _HTTP2_H_
+#define _HTTP2_H_
 
 #include <stddef.h>
 #include <stdint.h>
@@ -30,6 +30,11 @@ extern "C" {
 struct http2_ctx;
 struct http2_stream;
 
+/* HTTP/2 Settings structure */
+struct http2_settings {
+	int max_concurrent_streams; /* -1 = use default (4096), 0 = unlimited */
+};
+
 /* Error codes */
 enum {
 	HTTP2_ERR_NONE = 0,
@@ -37,8 +42,12 @@ enum {
 	HTTP2_ERR_EOF = -2,
 	HTTP2_ERR_IO = -3,
 	HTTP2_ERR_PROTOCOL = -4,
+	HTTP2_ERR_HTTP1 = -5,
 };
 
+/* Convert error code to string */
+const char *http2_error_to_string(int ret);
+
 /* BIO callback types */
 typedef int (*http2_bio_read_fn)(void *private_data, uint8_t *buf, int len);
 typedef int (*http2_bio_write_fn)(void *private_data, const uint8_t *buf, int len);
@@ -58,10 +67,11 @@ struct http2_poll_item {
  * @param bio_read Read callback function
  * @param bio_write Write callback function
  * @param private_data User data passed to BIO callbacks
+ * @param settings HTTP/2 settings to use (NULL for defaults)
  * @return New context or NULL on error
  */
 struct http2_ctx *http2_ctx_client_new(const char *server, http2_bio_read_fn bio_read, http2_bio_write_fn bio_write,
-									   void *private_data);
+									   void *private_data, const struct http2_settings *settings);
 
 /**
  * Create a new HTTP/2 server context
@@ -69,10 +79,11 @@ struct http2_ctx *http2_ctx_client_new(const char *server, http2_bio_read_fn bio
  * @param bio_read Read callback function
  * @param bio_write Write callback function
  * @param private_data User data passed to BIO callbacks
+ * @param settings HTTP/2 settings to use (NULL for defaults)
  * @return New context or NULL on error
  */
 struct http2_ctx *http2_ctx_server_new(const char *server, http2_bio_read_fn bio_read, http2_bio_write_fn bio_write,
-									   void *private_data);
+									   void *private_data, const struct http2_settings *settings);
 
 /**
  * Free an HTTP/2 context
@@ -80,6 +91,13 @@ struct http2_ctx *http2_ctx_server_new(const char *server, http2_bio_read_fn bio
  */
 void http2_ctx_free(struct http2_ctx *ctx);
 
+/**
+ * Close an HTTP/2 context and release all streams
+ * This is used to break circular references between context and streams
+ * @param ctx Context to close
+ */
+void http2_ctx_close(struct http2_ctx *ctx);
+
 /**
  * Increase reference count of HTTP/2 context
  * @param ctx HTTP/2 context
@@ -132,6 +150,13 @@ int http2_ctx_want_read(struct http2_ctx *ctx);
  */
 int http2_ctx_want_write(struct http2_ctx *ctx);
 
+/**
+ * Check if connection is closed or has encountered an error
+ * @param ctx HTTP/2 context
+ * @return 1 if connection is closed/errored, 0 if still active
+ */
+int http2_ctx_is_closed(struct http2_ctx *ctx);
+
 /* Stream Management APIs */
 
 /**
@@ -145,21 +170,21 @@ struct http2_stream *http2_stream_new(struct http2_ctx *ctx);
  * Free a stream
  * @param stream Stream to free
  */
-void http2_stream_free(struct http2_stream *stream);
+
 
 /**
  * Increase reference count of stream
  * @param stream Stream
  * @return The same stream pointer
  */
-struct http2_stream *http2_stream_ref(struct http2_stream *stream);
+struct http2_stream *http2_stream_get(struct http2_stream *stream);
 
 /**
  * Decrease reference count of stream
  * Frees the stream when reference count reaches zero
  * @param stream Stream
  */
-void http2_stream_unref(struct http2_stream *stream);
+void http2_stream_put(struct http2_stream *stream);
 
 /**
  * Get stream ID
@@ -205,6 +230,14 @@ int http2_stream_set_response(struct http2_stream *stream, int status, const str
  */
 const char *http2_stream_get_method(struct http2_stream *stream);
 
+/**
+ * Get query parameter from request path
+ * @param stream Stream
+ * @param name Parameter name
+ * @return Parameter value (must be freed by caller) or NULL if not found
+ */
+char *http2_stream_get_query_param(struct http2_stream *stream, const char *name);
+
 /**
  * Get request path
  * @param stream Stream
@@ -291,4 +324,4 @@ void *http2_stream_get_ex_data(struct http2_stream *stream);
 }
 #endif
 
-#endif /* _HTTP2_CLIENT_H_ */
+#endif /* _HTTP2_H_ */

+ 21 - 0
test/cases/test-bind.cc

@@ -320,3 +320,24 @@ server 127.0.0.1:63053 -group g2 -exclude-default-group
 	EXPECT_EQ(client.GetAnswer()[0].GetName(), "a.com");
 	EXPECT_EQ(client.GetAnswer()[0].GetData(), "5.6.7.8");
 }
+
+TEST(Bind, Get)
+{
+    smartdns::Server server;
+
+	int ret = system("which curl > /dev/null 2>&1");
+	if (ret != 0) {
+		GTEST_SKIP() << "curl not found, skip test";
+	}
+
+    // Start server with bind-https and logging to file
+    server.Start(R"""(bind-https [::]:60053 -alpn h2
+address /example.com/1.2.3.4
+log-level debug
+)""");
+
+    // Send GET request using curl
+    std::string cmd = "curl -k --http2 'https://127.0.0.1:60053/dns-query?dns=AAABAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE=' > /dev/null 2>&1";
+    ret = system(cmd.c_str());
+    ASSERT_EQ(ret, 0);
+}

+ 322 - 0
test/cases/test-http2.cc

@@ -0,0 +1,322 @@
+#include "client.h"
+#include "server.h"
+#include "smartdns/dns.h"
+#include "smartdns/http2.h"
+#include "gtest/gtest.h"
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <thread>
+
+// Test HTTP/2 with bind-https server (simulating upstream HTTPS server)
+TEST(HTTP2, BindServerHTTP2)
+{
+	Defer
+	{
+		unlink("/tmp/smartdns-cert.pem");
+		unlink("/tmp/smartdns-key.pem");
+	};
+
+	smartdns::Server server_wrap;
+	smartdns::Server server;
+
+	// Start main SmartDNS instance that queries upstream HTTPS server
+	server.Start(R"""(bind [::]:61053
+server https://127.0.0.1:60053/dns-query -no-check-certificate -alpn h2
+log-level debug
+)""");
+
+	// Start upstream HTTPS server (bind-https)
+	server_wrap.Start(R"""(bind-https [::]:60053 -alpn h2
+address /example.com/1.2.3.4
+address /test.com/5.6.7.8
+log-level debug
+)""");
+
+	smartdns::Client client;
+
+	// Test first query
+	ASSERT_TRUE(client.Query("example.com", 61053));
+	std::cout << client.GetResult() << std::endl;
+	ASSERT_EQ(client.GetAnswerNum(), 1);
+	EXPECT_EQ(client.GetStatus(), "NOERROR");
+	EXPECT_EQ(client.GetAnswer()[0].GetData(), "1.2.3.4");
+
+	// Test second query to verify connection reuse
+	ASSERT_TRUE(client.Query("test.com", 61053));
+	std::cout << client.GetResult() << std::endl;
+	ASSERT_EQ(client.GetAnswerNum(), 1);
+	EXPECT_EQ(client.GetStatus(), "NOERROR");
+	EXPECT_EQ(client.GetAnswer()[0].GetData(), "5.6.7.8");
+}
+
+TEST(HTTP2, ServerMultiStream)
+{
+	smartdns::Server server_wrap;
+	smartdns::Server server;
+
+	// Start main SmartDNS instance
+	server.Start(R"""(bind [::]:61053
+server https://127.0.0.1:60053/dns-query -no-check-certificate -alpn h2
+log-level debug
+)""");
+
+	// Start upstream HTTPS server (bind-https)
+	server_wrap.Start(R"""(bind-https [::]:60053 -alpn h2
+address /example.com/1.2.3.4
+address /test.com/5.6.7.8
+log-level debug
+)""");
+
+	smartdns::Client client;
+	
+	// Send multiple concurrent queries
+	// Note: The smartdns::Client might be synchronous, so we might need threads or a way to send async.
+	// But we can verify that multiple queries on the same connection work (multiplexing).
+	// The previous test already verified connection reuse.
+	// To verify concurrency, we'd need to delay the response on the server, which is hard with bind-https.
+	// However, we can at least verify that sending many queries quickly works.
+	
+	for (int i = 0; i < 10; i++) {
+		ASSERT_TRUE(client.Query("example.com", 61053));
+		EXPECT_EQ(client.GetStatus(), "NOERROR");
+		EXPECT_EQ(client.GetAnswer()[0].GetData(), "1.2.3.4");
+	}
+}
+
+TEST(HTTP2, ServerALPNConfig)
+{
+	smartdns::Server server_wrap;
+	smartdns::Server server;
+
+	// Case 1: Server supports h2, client requests h2 -> h2
+	server.Start(R"""(bind [::]:61053
+server https://127.0.0.1:60053/dns-query -no-check-certificate -alpn h2
+log-level debug
+)""");
+
+	server_wrap.Start(R"""(bind-https [::]:60053 -alpn h2
+address /example.com/1.2.3.4
+log-level debug
+)""");
+
+	smartdns::Client client;
+	ASSERT_TRUE(client.Query("example.com", 61053));
+	EXPECT_EQ(client.GetStatus(), "NOERROR");
+}
+
+TEST(HTTP2, ServerALPNFallback)
+{
+	smartdns::Server server_wrap;
+	smartdns::Server server;
+
+	// Case 2: Server supports http/1.1 only, client requests h2 -> fallback or fail?
+	// If client requests h2 only, it should fail.
+	// If client requests h2,http/1.1, it should fallback.
+	
+	server.Start(R"""(bind [::]:61053
+server https://127.0.0.1:60053/dns-query -no-check-certificate -alpn h2,http/1.1
+log-level debug
+)""");
+
+	server_wrap.Start(R"""(bind-https [::]:60053 -alpn http/1.1
+address /example.com/1.2.3.4
+log-level debug
+)""");
+
+	smartdns::Client client;
+	ASSERT_TRUE(client.Query("example.com", 61053));
+	EXPECT_EQ(client.GetStatus(), "NOERROR");
+}
+
+// Test client only supports HTTP/1.1, server supports both
+TEST(HTTP2, ClientHTTP1Only)
+{
+	smartdns::Server server_wrap;
+	smartdns::Server server;
+
+	// Client only supports http/1.1, server supports both h2 and http/1.1
+	server.Start(R"""(bind [::]:61053
+server https://127.0.0.1:60053/dns-query -no-check-certificate -alpn http/1.1
+log-level debug
+)""");
+
+	server_wrap.Start(R"""(bind-https [::]:60053 -alpn h2,http/1.1
+address /example.com/1.2.3.4
+log-level debug
+)""");
+
+	smartdns::Client client;
+	ASSERT_TRUE(client.Query("example.com", 61053));
+	EXPECT_EQ(client.GetStatus(), "NOERROR");
+	EXPECT_EQ(client.GetAnswer()[0].GetData(), "1.2.3.4");
+}
+
+// Test both client and server only support HTTP/1.1
+TEST(HTTP2, BothHTTP1Only)
+{
+	smartdns::Server server_wrap;
+	smartdns::Server server;
+
+	// Both client and server only support http/1.1
+	server.Start(R"""(bind [::]:61053
+server https://127.0.0.1:60053/dns-query -no-check-certificate -alpn http/1.1
+log-level debug
+)""");
+
+	server_wrap.Start(R"""(bind-https [::]:60053 -alpn http/1.1
+address /example.com/1.2.3.4
+address /test2.com/9.10.11.12
+log-level debug
+)""");
+
+	smartdns::Client client;
+	
+	// First query
+	ASSERT_TRUE(client.Query("example.com", 61053));
+	EXPECT_EQ(client.GetStatus(), "NOERROR");
+	EXPECT_EQ(client.GetAnswer()[0].GetData(), "1.2.3.4");
+	
+	// Second query to verify connection reuse with HTTP/1.1
+	ASSERT_TRUE(client.Query("test2.com", 61053));
+	EXPECT_EQ(client.GetStatus(), "NOERROR");
+	EXPECT_EQ(client.GetAnswer()[0].GetData(), "9.10.11.12");
+}
+
+// Test concurrent queries from multiple clients
+TEST(HTTP2, ConcurrentClients)
+{
+	smartdns::Server server_wrap;
+	smartdns::Server server;
+
+	server.Start(R"""(bind [::]:61053
+server https://127.0.0.1:60053/dns-query -no-check-certificate -alpn h2
+log-level debug
+)""");
+
+	server_wrap.Start(R"""(bind-https [::]:60053 -alpn h2
+address /example.com/1.2.3.4
+address /test.com/5.6.7.8
+log-level debug
+)""");
+
+	// Create multiple threads to query simultaneously
+	std::vector<std::thread> threads;
+	std::atomic<int> success_count{0};
+	
+	for (int i = 0; i < 5; i++) {
+		threads.emplace_back([&success_count, i]() {
+			smartdns::Client client;
+			const char* domain = (i % 2 == 0) ? "example.com" : "test.com";
+			const char* expected_ip = (i % 2 == 0) ? "1.2.3.4" : "5.6.7.8";
+			
+			if (client.Query(domain, 61053)) {
+				if (client.GetStatus() == "NOERROR" && 
+					client.GetAnswerNum() > 0 &&
+					client.GetAnswer()[0].GetData() == expected_ip) {
+					success_count++;
+				}
+			}
+		});
+	}
+	
+	for (auto& t : threads) {
+		t.join();
+	}
+	
+	EXPECT_EQ(success_count.load(), 5);
+}
+
+// Test mixed HTTP/2 and HTTP/1.1 queries
+TEST(HTTP2, MixedProtocolQueries)
+{
+	smartdns::Server server_wrap_h2;
+	smartdns::Server server_wrap_http1;
+	smartdns::Server server;
+
+	// Main server supports both protocols
+	server.Start(R"""(bind [::]:61053
+server https://127.0.0.1:60053/dns-query -no-check-certificate -alpn h2,http/1.1
+server https://127.0.0.1:60054/dns-query -no-check-certificate -alpn http/1.1
+log-level debug
+)""");
+
+	// First upstream supports HTTP/2
+	server_wrap_h2.Start(R"""(bind-https [::]:60053 -alpn h2
+address /h2-domain.com/1.1.1.1
+log-level debug
+)""");
+
+	// Second upstream supports HTTP/1.1 only
+	server_wrap_http1.Start(R"""(bind-https [::]:60054 -alpn http/1.1
+address /http1-domain.com/2.2.2.2
+log-level debug
+)""");
+
+	smartdns::Client client;
+	
+	// Query from HTTP/2 server
+	ASSERT_TRUE(client.Query("h2-domain.com", 61053));
+	EXPECT_EQ(client.GetStatus(), "NOERROR");
+	EXPECT_EQ(client.GetAnswer()[0].GetData(), "1.1.1.1");
+	
+	// Query from HTTP/1.1 server
+	ASSERT_TRUE(client.Query("http1-domain.com", 61053));
+	EXPECT_EQ(client.GetStatus(), "NOERROR");
+	EXPECT_EQ(client.GetAnswer()[0].GetData(), "2.2.2.2");
+}
+
+// Test connection reuse for HTTP/2
+TEST(HTTP2, ConnectionReuse)
+{
+	smartdns::Server server_wrap;
+	smartdns::Server server;
+
+	server.Start(R"""(bind [::]:61053
+server https://127.0.0.1:60053/dns-query -no-check-certificate -alpn h2
+log-level debug
+)""");
+
+	server_wrap.Start(R"""(bind-https [::]:60053 -alpn h2
+address /domain1.com/1.1.1.1
+address /domain2.com/2.2.2.2
+address /domain3.com/3.3.3.3
+log-level debug
+)""");
+
+	smartdns::Client client;
+	
+	// Multiple queries that should reuse the same HTTP/2 connection
+	for (int i = 1; i <= 3; i++) {
+		std::string domain = "domain" + std::to_string(i) + ".com";
+		std::string expected_ip = std::to_string(i) + "." + std::to_string(i) + "." + std::to_string(i) + "." + std::to_string(i);
+		
+		ASSERT_TRUE(client.Query(domain.c_str(), 61053));
+		EXPECT_EQ(client.GetStatus(), "NOERROR");
+		EXPECT_EQ(client.GetAnswer()[0].GetData(), expected_ip);
+	}
+}
+
+// Test default ALPN behavior (no explicit -alpn parameter)
+TEST(HTTP2, DefaultALPN)
+{
+	smartdns::Server server_wrap;
+	smartdns::Server server;
+
+	// Client doesn't specify ALPN (should default to supporting both)
+	server.Start(R"""(bind [::]:61053
+server https://127.0.0.1:60053/dns-query -no-check-certificate
+log-level debug
+)""");
+
+	// Server supports both (no explicit -alpn, should default to h2,http/1.1)
+	server_wrap.Start(R"""(bind-https [::]:60053
+address /example.com/1.2.3.4
+log-level debug
+)""");
+
+	smartdns::Client client;
+	ASSERT_TRUE(client.Query("example.com", 61053));
+	EXPECT_EQ(client.GetStatus(), "NOERROR");
+	EXPECT_EQ(client.GetAnswer()[0].GetData(), "1.2.3.4");
+}

+ 121 - 9
test/cases/test-lib-http2.cc

@@ -71,7 +71,7 @@ TEST_F(LIBHTTP2, Integrated)
 {
 	std::thread server_thread([this]() {
 		// Server logic
-		struct http2_ctx *ctx = http2_ctx_server_new("test-server", bio_read, bio_write, &server_sock);
+		struct http2_ctx *ctx = http2_ctx_server_new("test-server", bio_read, bio_write, &server_sock, NULL);
 		ASSERT_NE(ctx, nullptr);
 
 		// Handshake
@@ -143,7 +143,7 @@ TEST_F(LIBHTTP2, Integrated)
 
 	std::thread client_thread([this]() {
 		usleep(500000); // Wait for server start
-		struct http2_ctx *ctx = http2_ctx_client_new("test-client", bio_read, bio_write, &client_sock);
+		struct http2_ctx *ctx = http2_ctx_client_new("test-client", bio_read, bio_write, &client_sock, NULL);
 		ASSERT_NE(ctx, nullptr);
 
 		// Handshake
@@ -204,7 +204,7 @@ TEST_F(LIBHTTP2, Integrated)
 		std::string resp((char *)response_body, response_body_len);
 		EXPECT_NE(resp.find("Echo Response"), std::string::npos);
 
-		http2_stream_free(stream);
+		http2_stream_put(stream);
 		http2_ctx_free(ctx);
 	});
 
@@ -217,7 +217,7 @@ TEST_F(LIBHTTP2, MultiStream)
 	const int NUM_STREAMS = 3;
 
 	std::thread server_thread([this, NUM_STREAMS]() {
-		struct http2_ctx *ctx = http2_ctx_server_new("test-server", bio_read, bio_write, &server_sock);
+		struct http2_ctx *ctx = http2_ctx_server_new("test-server", bio_read, bio_write, &server_sock, NULL);
 		ASSERT_NE(ctx, nullptr);
 
 		// Handshake
@@ -275,7 +275,7 @@ TEST_F(LIBHTTP2, MultiStream)
 
 	std::thread client_thread([this, NUM_STREAMS]() {
 		usleep(50000);
-		struct http2_ctx *ctx = http2_ctx_client_new("test-client", bio_read, bio_write, &client_sock);
+		struct http2_ctx *ctx = http2_ctx_client_new("test-client", bio_read, bio_write, &client_sock, NULL);
 		ASSERT_NE(ctx, nullptr);
 
 		// Handshake
@@ -341,7 +341,7 @@ TEST_F(LIBHTTP2, MultiStream)
 		EXPECT_EQ(streams_completed, NUM_STREAMS);
 
 		for (int i = 0; i < NUM_STREAMS; i++) {
-			http2_stream_free(streams[i]);
+			http2_stream_put(streams[i]);
 		}
 		http2_ctx_free(ctx);
 	});
@@ -354,7 +354,7 @@ TEST_F(LIBHTTP2, EarlyStreamCreation)
 {
 	std::thread server_thread([this]() {
 		// Server logic
-		struct http2_ctx *ctx = http2_ctx_server_new("test-server", bio_read, bio_write, &server_sock);
+		struct http2_ctx *ctx = http2_ctx_server_new("test-server", bio_read, bio_write, &server_sock, NULL);
 		ASSERT_NE(ctx, nullptr);
 
 		// Handshake
@@ -432,7 +432,7 @@ TEST_F(LIBHTTP2, EarlyStreamCreation)
 		usleep(50000); // Wait for server start
 
 		// Create client context
-		struct http2_ctx *ctx = http2_ctx_client_new("test-client", bio_read, bio_write, &client_sock);
+		struct http2_ctx *ctx = http2_ctx_client_new("test-client", bio_read, bio_write, &client_sock, NULL);
 		ASSERT_NE(ctx, nullptr);
 
 		// IMPORTANT: Create stream and send request BEFORE handshake completes
@@ -495,7 +495,119 @@ TEST_F(LIBHTTP2, EarlyStreamCreation)
 		EXPECT_NE(resp.find("Echo Response"), std::string::npos);
 		EXPECT_NE(resp.find("test echo"), std::string::npos);
 
-		http2_stream_free(stream);
+		http2_stream_put(stream);
+		http2_ctx_free(ctx);
+	});
+
+	server_thread.join();
+	client_thread.join();
+}
+
+TEST_F(LIBHTTP2, ServerLoopTerminationOnDisconnect)
+{
+	std::thread server_thread([this]() {
+		struct http2_ctx *ctx = http2_ctx_server_new("test-server", bio_read, bio_write, &server_sock, NULL);
+		ASSERT_NE(ctx, nullptr);
+
+		// Handshake
+		int handshake_attempts = 200;
+		int ret = 0;
+		while (handshake_attempts-- > 0) {
+			struct pollfd pfd = {server_sock, POLLIN, 0};
+			int poll_ret = poll(&pfd, 1, 10);
+			if (poll_ret == 0) {
+				continue;
+			}
+			ret = http2_ctx_handshake(ctx);
+			if (ret == 1)
+				break;
+			if (ret < 0)
+				break;
+		}
+		ASSERT_EQ(ret, 1) << "Server handshake failed";
+
+		// Accept stream
+		struct http2_stream *stream = nullptr;
+		int max_attempts = 200;
+		while (max_attempts-- > 0 && !stream) {
+			struct pollfd pfd = {server_sock, POLLIN, 0};
+			poll(&pfd, 1, 100);
+			struct http2_poll_item items[10];
+			int count = 0;
+			http2_ctx_poll(ctx, items, 10, &count);
+			for (int i = 0; i < count; i++) {
+				if (items[i].stream == nullptr && items[i].readable) {
+					stream = http2_ctx_accept_stream(ctx);
+					if (stream)
+						break;
+				}
+			}
+			usleep(20000);
+		}
+		ASSERT_NE(stream, nullptr) << "Server failed to accept stream";
+
+		// Read request body until EOF
+		uint8_t buf[1024];
+		int loop_count = 0;
+		while (loop_count++ < 100) {
+			struct http2_poll_item items[10];
+			int count = 0;
+			http2_ctx_poll(ctx, items, 10, &count);
+			
+			int data_read = 0;
+			for (int i = 0; i < count; i++) {
+				if (items[i].stream == stream && items[i].readable) {
+					int ret = http2_stream_read_body(stream, buf, sizeof(buf));
+					if (ret > 0) {
+						data_read = 1;
+					} else if (ret == 0) {
+						// EOF received
+						data_read = 1;
+					}
+				}
+			}
+			
+			if (!data_read && http2_stream_is_end(stream)) {
+				// If we are here, it means poll returned 0 items (or stream not readable),
+				// which is correct behavior after EOF is consumed.
+				// If the bug exists, poll would keep returning readable stream, and we would keep reading 0 bytes.
+				break;
+			}
+			
+			usleep(10000);
+		}
+		
+		EXPECT_LT(loop_count, 100) << "Server loop did not terminate (infinite loop detected)";
+
+		http2_ctx_free(ctx);
+	});
+
+	std::thread client_thread([this]() {
+		usleep(50000);
+		struct http2_ctx *ctx = http2_ctx_client_new("test-client", bio_read, bio_write, &client_sock, NULL);
+		ASSERT_NE(ctx, nullptr);
+
+		int handshake_attempts = 200;
+		int ret = 0;
+		while (handshake_attempts-- > 0) {
+			struct pollfd pfd = {client_sock, POLLIN, 0};
+			poll(&pfd, 1, 10);
+			ret = http2_ctx_handshake(ctx);
+			if (ret == 1) break;
+			if (ret < 0) break;
+		}
+		ASSERT_EQ(ret, 1);
+
+		struct http2_stream *stream = http2_stream_new(ctx);
+		ASSERT_NE(stream, nullptr);
+
+		struct http2_header_pair headers[] = {{"content-type", "text/plain"}, {NULL, NULL}};
+		http2_stream_set_request(stream, "POST", "/test", headers);
+		http2_stream_write_body(stream, (const uint8_t *)"test", 4, 1);
+
+		usleep(200000); // Wait for server to process
+		
+		http2_stream_put(stream);
 		http2_ctx_free(ctx);
 	});