commit a90133fa349698587dcf7eb71bb66eb9e9219cc4
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-06-01T04:06:18Z |
| subject | IPv6 support |
commit a90133fa349698587dcf7eb71bb66eb9e9219cc4
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-06-01T04:06:18Z
IPv6 support
---
examples/ipv6.conf | 7 ++++
src/datagram_client.c | 28 ++++++++------
src/datagram_listener.c | 99 ++++++++++++++++++++++++++++++++++++++-----------
src/endpoint.c | 47 ++++++++++++++++++++++-
src/endpoint.h | 6 ++-
src/file_conf.c | 82 +++++++++++++++++++++++++++++++---------
src/stream_conn.c | 31 ++++++++--------
src/stream_listener.c | 16 ++++----
8 files changed, 241 insertions(+), 75 deletions(-)
diff --git a/examples/ipv6.conf b/examples/ipv6.conf
new file mode 100644
index 0000000..4bdf4ea
--- /dev/null
+++ b/examples/ipv6.conf
@@ -0,0 +1,7 @@
+# v6 to v4
+tcp :48899 builtin http_ok
+tcp [::1]:48892 tcp 127.0.0.1:48899
+tcp [::1]:48891 tcp [::1]:48892
+
+udp [::1]:9292 builtin http_ok
+udp [::1]:9293 udp [::1]:9292
diff --git a/src/datagram_client.c b/src/datagram_client.c
index 3ec38ac..de7a523 100644
--- a/src/datagram_client.c
+++ b/src/datagram_client.c
@@ -100,18 +100,19 @@ static int connect_datagram_upstream(struct datagram_client *c, const struct end
switch (upstream->kind) {
- case ENDPOINT_INET: {
- struct sockaddr_in upstream_addr;
-
- memset(&upstream_addr, 0, sizeof(upstream_addr));
- upstream_addr.sin_family = AF_INET;
- upstream_addr.sin_port = htons(upstream->port);
+ case ENDPOINT_INET:
+ case ENDPOINT_INET6:
+ {
+ struct sockaddr_storage upstream_addr;
+ socklen_t upstream_addr_len;
+ int rc;
- if (inet_pton(AF_INET, upstream->host, &upstream_addr.sin_addr) != 1) {
- return -EINVAL;
+ rc = endpoint_to_sockaddr(upstream, &upstream_addr, &upstream_addr_len);
+ if (rc != 0) {
+ return rc;
}
- c->fd = socket(AF_INET, SOCK_DGRAM, 0);
+ c->fd = socket(upstream_addr.ss_family, SOCK_DGRAM, 0);
if (c->fd < 0) {
return -EVUTIL_SOCKET_ERROR();
}
@@ -119,9 +120,14 @@ static int connect_datagram_upstream(struct datagram_client *c, const struct end
if (connect(
c->fd,
(const struct sockaddr *)&upstream_addr,
- sizeof(upstream_addr)
+ upstream_addr_len
) < 0) {
- return -EVUTIL_SOCKET_ERROR();
+ int err = EVUTIL_SOCKET_ERROR();
+
+ evutil_closesocket(c->fd);
+ c->fd = -1;
+
+ return -err;
}
return 0;
diff --git a/src/datagram_listener.c b/src/datagram_listener.c
index 1b0cc8b..6aaf141 100644
--- a/src/datagram_listener.c
+++ b/src/datagram_listener.c
@@ -331,22 +331,37 @@ static int bind_unix_datagram_listener(struct datagram_route_ctx *ctx)
static int bind_udp_datagram_listener(struct datagram_route_ctx *ctx)
{
const struct route *r = ctx->route;
- struct sockaddr_in listen_addr;
+ struct sockaddr_storage listen_addr;
+ socklen_t listen_addr_len;
+ int family;
int one = 1;
+ int rc;
- memset(&listen_addr, 0, sizeof(listen_addr));
+ rc = endpoint_to_sockaddr(&r->listen, &listen_addr, &listen_addr_len);
+ if (rc != 0) {
+ LOG_ERROR("invalid udp listen address",
+ "listen", _LOGV_ENDPOINT(&r->listen)
+ );
+ return rc;
+ }
- listen_addr.sin_family = AF_INET;
- listen_addr.sin_port = htons(r->listen.port);
+ switch (r->listen.kind) {
+ case ENDPOINT_INET:
+ family = AF_INET;
+ break;
- if (inet_pton(AF_INET, r->listen.host, &listen_addr.sin_addr) != 1) {
- LOG_ERROR("invalid udp listen address",
+ case ENDPOINT_INET6:
+ family = AF_INET6;
+ break;
+
+ default:
+ LOG_ERROR("invalid udp listen endpoint kind",
"listen", _LOGV_ENDPOINT(&r->listen)
);
return -EINVAL;
}
- ctx->listen_fd = socket(AF_INET, SOCK_DGRAM, 0);
+ ctx->listen_fd = socket(family, SOCK_DGRAM, 0);
if (ctx->listen_fd < 0) {
int err = EVUTIL_SOCKET_ERROR();
@@ -357,13 +372,46 @@ static int bind_udp_datagram_listener(struct datagram_route_ctx *ctx)
}
if (r->opts.broadcast_reply != BROADCAST_REPLY_OFF) {
+ if (r->listen.kind != ENDPOINT_INET) {
+ LOG_ERROR("broadcast_reply is only supported for IPv4 UDP",
+ "line", _LOGV(r->line_no),
+ "listen", _LOGV_ENDPOINT(&r->listen)
+ );
+ return -EINVAL;
+ }
+
+ {
+ int yes = 1;
+
+ if (setsockopt(ctx->listen_fd, SOL_SOCKET, SO_BROADCAST,
+ (const char *)&yes, sizeof(yes)) < 0) {
+ int err = EVUTIL_SOCKET_ERROR();
+
+ LOG_ERROR("failed to enable broadcast",
+ "line", _LOGV(r->line_no),
+ "err", _LOGV(evutil_socket_error_to_string(err))
+ );
+ return -err;
+ }
+ }
+ }
+
+ if (family == AF_INET6) {
int yes = 1;
- if (setsockopt(ctx->listen_fd, SOL_SOCKET, SO_BROADCAST,
+
+ /*
+ * Keep behavior explicit:
+ * udp :1234 => IPv4 only
+ * udp [::]:1234 => IPv6 only
+ */
+ if (setsockopt(ctx->listen_fd, IPPROTO_IPV6, IPV6_V6ONLY,
(const char *)&yes, sizeof(yes)) < 0) {
int err = EVUTIL_SOCKET_ERROR();
- LOG_ERROR("failed to enable broadcast",
- "line", _LOGV(r->line_no),
- "err", _LOGV(evutil_socket_error_to_string(err)));
+
+ LOG_ERROR("failed to set IPV6_V6ONLY",
+ "line", _LOGV(r->line_no),
+ "err", _LOGV(evutil_socket_error_to_string(err))
+ );
return -err;
}
}
@@ -375,25 +423,33 @@ static int bind_udp_datagram_listener(struct datagram_route_ctx *ctx)
return -EINVAL;
}
- setsockopt(
- ctx->listen_fd,
- SOL_SOCKET,
- SO_REUSEADDR,
- (const char *)&one,
- sizeof(one)
- );
+ if (setsockopt(
+ ctx->listen_fd,
+ SOL_SOCKET,
+ SO_REUSEADDR,
+ (const char *)&one,
+ sizeof(one)
+ ) < 0) {
+ int err = EVUTIL_SOCKET_ERROR();
+
+ LOG_ERROR("failed to set SO_REUSEADDR",
+ "line", _LOGV(r->line_no),
+ "err", _LOGV(evutil_socket_error_to_string(err))
+ );
+ return -err;
+ }
if (bind(
ctx->listen_fd,
(const struct sockaddr *)&listen_addr,
- sizeof(listen_addr)
+ listen_addr_len
) < 0) {
int err = EVUTIL_SOCKET_ERROR();
LOG_ERROR("udp bind failed",
"err", _LOGV(evutil_socket_error_to_string(err))
);
- return -EADDRINUSE;
+ return -err;
}
ctx->local_addr_len = sizeof(ctx->local_addr);
@@ -409,7 +465,7 @@ static int bind_udp_datagram_listener(struct datagram_route_ctx *ctx)
LOG_ERROR("udp getsockname failed",
"err", _LOGV(evutil_socket_error_to_string(err))
);
- return -EINVAL;
+ return -err;
}
ctx->listen_ev = event_new(
@@ -436,6 +492,7 @@ int bind_datagram_listener(struct datagram_route_ctx *ctx)
{
switch (ctx->route->listen.kind) {
case ENDPOINT_INET:
+ case ENDPOINT_INET6:
return bind_udp_datagram_listener(ctx);
case ENDPOINT_UNIX_DGRAM:
diff --git a/src/endpoint.c b/src/endpoint.c
index 53ca6fc..863522a 100644
--- a/src/endpoint.c
+++ b/src/endpoint.c
@@ -1,6 +1,7 @@
+#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
-#include <errno.h>
+#include <string.h>
#include "endpoint.h"
@@ -20,6 +21,10 @@ int endpoint_to_string(const struct endpoint *ep, char *buf, size_t buf_len)
snprintf(buf, buf_len, "%s:%u", ep->host, ep->port);
return 0;
+ case ENDPOINT_INET6:
+ snprintf(buf, buf_len, "[%s]:%u", ep->host, ep->port);
+ return 0;
+
case ENDPOINT_UNIX:
snprintf(buf, buf_len, "unix:%s", ep->path);
return 0;
@@ -42,6 +47,46 @@ int endpoint_to_string(const struct endpoint *ep, char *buf, size_t buf_len)
}
}
+int endpoint_to_sockaddr(const struct endpoint *ep,
+ struct sockaddr_storage *ss,
+ socklen_t *ss_len)
+{
+ memset(ss, 0, sizeof(*ss));
+
+ switch (ep->kind) {
+ case ENDPOINT_INET: {
+ struct sockaddr_in *addr4 = (struct sockaddr_in *)ss;
+
+ addr4->sin_family = AF_INET;
+ addr4->sin_port = htons(ep->port);
+
+ if (inet_pton(AF_INET, ep->host, &addr4->sin_addr) != 1) {
+ return -EINVAL;
+ }
+
+ *ss_len = sizeof(*addr4);
+ return 0;
+ }
+
+ case ENDPOINT_INET6: {
+ struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)ss;
+
+ addr6->sin6_family = AF_INET6;
+ addr6->sin6_port = htons(ep->port);
+
+ if (inet_pton(AF_INET6, ep->host, &addr6->sin6_addr) != 1) {
+ return -EINVAL;
+ }
+
+ *ss_len = sizeof(*addr6);
+ return 0;
+ }
+
+ default:
+ return -EINVAL;
+ }
+}
+
bool endpoint_is_stream(const struct endpoint *ep)
{
if (ep == NULL) {
diff --git a/src/endpoint.h b/src/endpoint.h
index 867e92a..3624430 100644
--- a/src/endpoint.h
+++ b/src/endpoint.h
@@ -3,6 +3,7 @@
#include <stdint.h>
+#include "compat.h"
#include "x_builtins.h"
enum endpoint_proto {
@@ -16,6 +17,7 @@ enum endpoint_proto {
enum endpoint_kind {
ENDPOINT_INET,
+ ENDPOINT_INET6,
ENDPOINT_UNIX,
ENDPOINT_UNIX_DGRAM,
ENDPOINT_BUILTIN,
@@ -40,5 +42,7 @@ int endpoint_to_string(const struct endpoint *ep, char *buf, size_t buf_len);
bool endpoint_is_stream(const struct endpoint *ep);
bool endpoint_is_datagram(const struct endpoint *ep);
bool endpoint_is_listenable(const struct endpoint *ep);
-
+int endpoint_to_sockaddr(const struct endpoint *ep,
+ struct sockaddr_storage *ss,
+ socklen_t *ss_len);
#endif
diff --git a/src/file_conf.c b/src/file_conf.c
index 8b6ae5a..65fe613 100644
--- a/src/file_conf.c
+++ b/src/file_conf.c
@@ -86,30 +86,74 @@ static int find_conflicting_route(
static int split_host_port(const char *input,
char *host, size_t host_len,
- uint16_t *port)
+ uint16_t *port,
+ bool *is_ipv6)
{
- const char *colon = strrchr(input, ':');
- if (!colon) {
+ const char *port_s;
+ size_t hlen;
+
+ if (!input || !host || host_len == 0 || !port || !is_ipv6) {
return -1;
}
- size_t hlen = (size_t)(colon - input);
- const char *port_s = colon + 1;
+ host[0] = '\0';
+ *is_ipv6 = false;
- if (hlen >= host_len || *port_s == '\0') {
- return -1;
- }
+ if (input[0] == '[') {
+ const char *close = strchr(input, ']');
- memcpy(host, input, hlen);
- host[hlen] = '\0';
+ if (!close) {
+ return -1;
+ }
- if (parse_port(port_s, port) != 0) {
- return -1;
+ if (close[1] != ':') {
+ return -1;
+ }
+
+ hlen = (size_t)(close - (input + 1));
+ port_s = close + 2;
+
+ if (hlen == 0 || hlen >= host_len || *port_s == '\0') {
+ return -1;
+ }
+
+ memcpy(host, input + 1, hlen);
+ host[hlen] = '\0';
+ *is_ipv6 = true;
+ } else {
+ const char *first_colon = strchr(input, ':');
+ const char *last_colon = strrchr(input, ':');
+
+ if (!last_colon) {
+ return -1;
+ }
+
+ /*
+ * Reject bare IPv6 with port.
+ * Use [::1]:80 instead.
+ */
+ if (first_colon != last_colon) {
+ return -1;
+ }
+
+ hlen = (size_t)(last_colon - input);
+ port_s = last_colon + 1;
+
+ if (hlen >= host_len || *port_s == '\0') {
+ return -1;
+ }
+
+ memcpy(host, input, hlen);
+ host[hlen] = '\0';
+
+ /* Allow ":80" as IPv4 wildcard shorthand. */
+ if (host[0] == '\0') {
+ snprintf(host, host_len, "0.0.0.0");
+ }
}
- // Allow ":80" as shorthand for all interfaces.
- if (host[0] == '\0') {
- snprintf(host, host_len, "0.0.0.0");
+ if (parse_port(port_s, port) != 0) {
+ return -1;
}
return 0;
@@ -301,14 +345,16 @@ static int parse_endpoint(enum endpoint_proto proto, const char *s, struct endpo
switch (proto) {
case PROTO_TCP:
- case PROTO_UDP:
- ep->kind = ENDPOINT_INET;
+ case PROTO_UDP: {
+ bool is_ipv6 = false;
- if (split_host_port(s, ep->host, sizeof(ep->host), &ep->port) != 0) {
+ if (split_host_port(s, ep->host, sizeof(ep->host), &ep->port, &is_ipv6) != 0) {
return -1;
}
+ ep->kind = is_ipv6 ? ENDPOINT_INET6 : ENDPOINT_INET;
return 0;
+ }
case PROTO_UNIX_STREAM:
path = s;
diff --git a/src/stream_conn.c b/src/stream_conn.c
index 464c247..e9bb34b 100644
--- a/src/stream_conn.c
+++ b/src/stream_conn.c
@@ -207,26 +207,27 @@ void stream_upstream_event_cb(struct bufferevent *bev, short events, void *arg)
static int connect_upstream(struct bufferevent *bev, const struct endpoint *ep)
{
- if (ep == NULL) {
+ if (bev == NULL || ep == NULL) {
return -EINVAL;
}
switch (ep->kind) {
- case ENDPOINT_INET: {
- struct sockaddr_in addr;
-
- memset(&addr, 0, sizeof(addr));
- addr.sin_family = AF_INET;
- addr.sin_port = htons(ep->port);
+ case ENDPOINT_INET:
+ case ENDPOINT_INET6:
+ {
+ struct sockaddr_storage addr;
+ socklen_t addr_len;
+ int rc;
- if (inet_pton(AF_INET, ep->host, &addr.sin_addr) != 1) {
- return -EINVAL;
+ rc = endpoint_to_sockaddr(ep, &addr, &addr_len);
+ if (rc != 0) {
+ return rc;
}
if (bufferevent_socket_connect(
- bev,
- (struct sockaddr *)&addr,
- sizeof(addr)) < 0) {
+ bev,
+ (struct sockaddr *)&addr,
+ addr_len) < 0) {
return -errno;
}
@@ -251,9 +252,9 @@ static int connect_upstream(struct bufferevent *bev, const struct endpoint *ep)
strcpy(addr.sun_path, ep->path);
if (bufferevent_socket_connect(
- bev,
- (struct sockaddr *)&addr,
- sizeof(addr)) < 0) {
+ bev,
+ (struct sockaddr *)&addr,
+ sizeof(addr)) < 0) {
return -errno;
}
diff --git a/src/stream_listener.c b/src/stream_listener.c
index d7e0732..8b79117 100644
--- a/src/stream_listener.c
+++ b/src/stream_listener.c
@@ -67,16 +67,15 @@ void accept_error_cb(struct evconnlistener *listener, void *arg)
static int bind_tcp_stream_listener(struct stream_route_ctx *ctx)
{
const struct route *r = ctx->route;
- struct sockaddr_in listen_addr;
-
- memset(&listen_addr, 0, sizeof(listen_addr));
- listen_addr.sin_family = AF_INET;
- listen_addr.sin_port = htons(r->listen.port);
+ struct sockaddr_storage listen_addr;
+ socklen_t listen_addr_len;
+ int rc;
- if (inet_pton(AF_INET, r->listen.host, &listen_addr.sin_addr) != 1) {
+ rc = endpoint_to_sockaddr(&r->listen, &listen_addr, &listen_addr_len);
+ if (rc != 0) {
LOG_ERROR("invalid listen address",
"listen", _LOGV_ENDPOINT(&r->listen));
- return -EINVAL;
+ return rc;
}
ctx->listener = evconnlistener_new_bind(
@@ -86,7 +85,7 @@ static int bind_tcp_stream_listener(struct stream_route_ctx *ctx)
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
-1,
(struct sockaddr *)&listen_addr,
- sizeof(listen_addr)
+ listen_addr_len
);
if (ctx->listener == NULL) {
@@ -217,6 +216,7 @@ int bind_stream_listener(struct stream_route_ctx *ctx)
{
switch (ctx->route->listen.kind) {
case ENDPOINT_INET:
+ case ENDPOINT_INET6:
return bind_tcp_stream_listener(ctx);
case ENDPOINT_UNIX: