From eb72e2a58d8c6cdffbd26229b5ae44bdff0b927b Mon Sep 17 00:00:00 2001 From: Jesse Awan Date: Tue, 27 Jan 2026 21:29:41 +0100 Subject: [PATCH 1/2] fix: Support IPv6 bind addresses in http_server helper Fixes URI::InvalidURIError when binding to IPv6 addresses like '::' or '::1'. The http_server helper was generating invalid URIs such as 'http://:::24231' instead of the RFC 3986 compliant 'http://[::]:24231'. Changes: - Wrap IPv6 addresses in brackets when constructing URIs - Add unit tests for IPv6 localhost (::1) and wildcard (::) binding - Update CHANGELOG.md This bug affected all Fluentd versions including v1.19.1 and caused crashes when attempting to bind HTTP servers to IPv6 addresses in dual-stack environments. Resolves: Invalid URI generation for IPv6 bind addresses Signed-off-by: Jesse Awan --- CHANGELOG.md | 9 +++++ .../plugin_helper/http_server/server.rb | 3 +- test/plugin_helper/test_http_server_helper.rb | 37 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9887cd190e..497ad9435b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # v1.19 +## Release v1.19.1 - TBD + +### Bug fix + +* http_server helper: Fix IPv6 bind address support in URI construction + * Fixed `URI::InvalidURIError` when binding to IPv6 addresses (e.g., `::`) + * IPv6 addresses are now properly bracketed in URIs per RFC 3986 (e.g., `http://[::]:24231`) + * Affects all plugins using http_server helper with IPv6 bind addresses + ## Release v1.19.0 - 2025/07/30 ### Enhancement diff --git a/lib/fluent/plugin_helper/http_server/server.rb b/lib/fluent/plugin_helper/http_server/server.rb index 9c099292d8..85ae74b58a 100644 --- a/lib/fluent/plugin_helper/http_server/server.rb +++ b/lib/fluent/plugin_helper/http_server/server.rb @@ -37,7 +37,8 @@ def initialize(addr:, port:, logger:, default_app: nil, tls_context: nil) # TODO: support http2 scheme = tls_context ? 'https' : 'http' - @uri = URI("#{scheme}://#{@addr}:#{@port}").to_s + addr_display = @addr.include?(":") ? "[#{@addr}]" : @addr + @uri = URI("#{scheme}://#{addr_display}:#{@port}").to_s @router = Router.new(default_app) @server_task = nil Console.logger = Fluent::Log::ConsoleAdapter.wrap(@logger) diff --git a/test/plugin_helper/test_http_server_helper.rb b/test/plugin_helper/test_http_server_helper.rb index bc9853c013..a101714c4c 100644 --- a/test/plugin_helper/test_http_server_helper.rb +++ b/test/plugin_helper/test_http_server_helper.rb @@ -21,6 +21,16 @@ def teardown @port = nil end + def ipv6_enabled? + begin + sock = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM, 0) + sock.close + true + rescue + false + end + end + class Dummy < Fluent::Plugin::TestBase helpers :http_server end @@ -364,6 +374,33 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr end end + test 'bind to IPv6 address' do + omit('IPv6 not supported') unless ipv6_enabled? + on_driver do |driver| + driver.http_server_create_http_server(:http_server_helper_test, addr: '::1', port: @port, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from ipv6'] } + end + + resp = get("http://[::1]:#{@port}/example/hello") + assert_equal('200', resp.code) + assert_equal('hello from ipv6', resp.body) + end + end + + test 'bind to IPv6 wildcard address' do + omit('IPv6 not supported') unless ipv6_enabled? + on_driver do |driver| + driver.http_server_create_http_server(:http_server_helper_test, addr: '::', port: @port, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from ipv6 wildcard'] } + end + + # Can access via IPv4-mapped IPv6 or IPv6 loopback + resp = get("http://[::1]:#{@port}/example/hello") + assert_equal('200', resp.code) + assert_equal('hello from ipv6 wildcard', resp.body) + end + end + test 'must be called #start and #stop' do on_driver do |driver| server = flexmock('Server') do |watcher| From 3aea00ea931d2354c235f6c5e5b01355d94b920b Mon Sep 17 00:00:00 2001 From: Jesse Awan Date: Wed, 28 Jan 2026 10:46:56 +0100 Subject: [PATCH 2/2] fix: Handle pre-bracketed IPv6 addresses to avoid double-bracketing - Updated server.rb to check if IPv6 addresses are already bracketed - Added tests for pre-bracketed addresses ([::1] and [::]) - Improved ipv6_enabled? helper to verify both binding and resolution - Updated CHANGELOG to document pre-bracketed address handling Signed-off-by: Jesse Awan --- CHANGELOG.md | 3 +- .../plugin_helper/http_server/server.rb | 10 +++++- test/plugin_helper/test_http_server_helper.rb | 36 +++++++++++++++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 497ad9435b..56b7ed674e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ ### Bug fix * http_server helper: Fix IPv6 bind address support in URI construction - * Fixed `URI::InvalidURIError` when binding to IPv6 addresses (e.g., `::`) + * Fixed `URI::InvalidURIError` when binding to IPv6 addresses (e.g., `::`, `::1`) * IPv6 addresses are now properly bracketed in URIs per RFC 3986 (e.g., `http://[::]:24231`) + * Handles pre-bracketed addresses correctly to avoid double-bracketing * Affects all plugins using http_server helper with IPv6 bind addresses ## Release v1.19.0 - 2025/07/30 diff --git a/lib/fluent/plugin_helper/http_server/server.rb b/lib/fluent/plugin_helper/http_server/server.rb index 85ae74b58a..b59450c242 100644 --- a/lib/fluent/plugin_helper/http_server/server.rb +++ b/lib/fluent/plugin_helper/http_server/server.rb @@ -37,7 +37,15 @@ def initialize(addr:, port:, logger:, default_app: nil, tls_context: nil) # TODO: support http2 scheme = tls_context ? 'https' : 'http' - addr_display = @addr.include?(":") ? "[#{@addr}]" : @addr + # Handle IPv6 addresses properly in URI construction per RFC 3986 + # Check if address is already bracketed to avoid double-bracketing + addr_display = if @addr.include?(":") && !@addr.start_with?('[') + "[#{@addr}]" # Bare IPv6 address - add brackets + elsif @addr.start_with?('[') && @addr.end_with?(']') + @addr # Already bracketed - use as-is + else + @addr # IPv4 or hostname - use directly + end @uri = URI("#{scheme}://#{addr_display}:#{@port}").to_s @router = Router.new(default_app) @server_task = nil diff --git a/test/plugin_helper/test_http_server_helper.rb b/test/plugin_helper/test_http_server_helper.rb index a101714c4c..f71400ded3 100644 --- a/test/plugin_helper/test_http_server_helper.rb +++ b/test/plugin_helper/test_http_server_helper.rb @@ -23,10 +23,16 @@ def teardown def ipv6_enabled? begin + # Try to actually bind to an IPv6 address to verify it works sock = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM, 0) + sock.bind(Socket.sockaddr_in(0, '::1')) sock.close + + # Also test that we can resolve IPv6 addresses + # This is needed because some systems can bind but can't connect + Socket.getaddrinfo('::1', nil, Socket::AF_INET6) true - rescue + rescue Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, SocketError false end end @@ -380,7 +386,6 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr driver.http_server_create_http_server(:http_server_helper_test, addr: '::1', port: @port, logger: NULL_LOGGER) do |s| s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from ipv6'] } end - resp = get("http://[::1]:#{@port}/example/hello") assert_equal('200', resp.code) assert_equal('hello from ipv6', resp.body) @@ -393,7 +398,6 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr driver.http_server_create_http_server(:http_server_helper_test, addr: '::', port: @port, logger: NULL_LOGGER) do |s| s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from ipv6 wildcard'] } end - # Can access via IPv4-mapped IPv6 or IPv6 loopback resp = get("http://[::1]:#{@port}/example/hello") assert_equal('200', resp.code) @@ -401,6 +405,32 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr end end + test 'handle already bracketed IPv6 address' do + omit('IPv6 not supported') unless ipv6_enabled? + on_driver do |driver| + # Test that pre-bracketed addresses don't get double-bracketed + driver.http_server_create_http_server(:http_server_helper_test, addr: '[::1]', port: @port, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from bracketed ipv6'] } + end + resp = get("http://[::1]:#{@port}/example/hello") + assert_equal('200', resp.code) + assert_equal('hello from bracketed ipv6', resp.body) + end + end + + test 'handle already bracketed IPv6 wildcard address' do + omit('IPv6 not supported') unless ipv6_enabled? + on_driver do |driver| + driver.http_server_create_http_server(:http_server_helper_test, addr: '[::]', port: @port, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from bracketed ipv6 wildcard'] } + end + # Access via IPv6 loopback + resp = get("http://[::1]:#{@port}/example/hello") + assert_equal('200', resp.code) + assert_equal('hello from bracketed ipv6 wildcard', resp.body) + end + end + test 'must be called #start and #stop' do on_driver do |driver| server = flexmock('Server') do |watcher|