Skip to content

Commit 03d9b91

Browse files
authored
fix: Add IPv6 support for bind parameter (#240)
* fix: Add IPv6 support for bind parameter Fixes two IPv6-related bugs when using bind='::': 1. Log message construction: Previously created invalid URL 'http://:::24231' instead of 'http://[::]:24231' 2. Worker aggregation: When bind is '::', the code tried to connect to '::' directly instead of converting it to '::1' (IPv6 localhost), causing connection failures Changes: - Add bracket notation for IPv6 addresses in log messages - Convert '::' to '::1' for inter-worker communication - Maintain backward compatibility with IPv4 addresses Fixes #229 Signed-off-by: Jesse Awan <jesse.awan@sap.com> * test: add IPv6 integration tests with shared examples - Add ipv6_enabled? helper to detect IPv6 support - Add integration tests for IPv6 loopback (::1), any (::), and pre-bracketed addresses - Refactor tests using shared_examples pattern to reduce duplication (60 → 35 lines) - Handle pre-bracketed addresses by stripping brackets before socket binding - Pass bracketed IPv6 addresses to http_server helper for proper URI construction - All IPv6 tests pass on systems with IPv6 support, skip gracefully otherwise - Add /vendor/ to .gitignore for local bundle installations Signed-off-by: Jesse Awan <jesse.awan@sap.com> * fix: bind raw IPv6, drop unit tests Signed-off-by: Jesse Awan <jesse.awan@sap.com> * fix: Support IPv6 bind addresses in prometheus input plugin - Route IPv6 addresses to webrick (http_server helper can't construct IPv6 URIs) - Add IPv6 + TLS guard (raises clear ConfigError for unsupported combination) - Fix start_webrick to handle non-SSL cases (prevents NoMethodError) - Add IPv6 URI bracketing in async_wrapper (formats as http://[::1]:port) - Add test for IPv6 + TLS error case - Fix FULL_CONFIG -> LOCAL_CONFIG in multi-worker test (FULL_CONFIG was undefined) Fixes Ruby 3.4 test failures with IPv6 addresses. All tests pass: 20 examples, 0 failures Signed-off-by: Jesse Awan <jesse.awan@sap.com> * Revert unrelated FULL_CONFIG change This line should not have been touched as part of the IPv6 fix. Keeping the original FULL_CONFIG as requested. Signed-off-by: Jesse Awan <jesse.awan@sap.com> --------- Signed-off-by: Jesse Awan <jesse.awan@sap.com>
1 parent b3cd6ff commit 03d9b91

File tree

5 files changed

+131
-23
lines changed

5 files changed

+131
-23
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414
*.o
1515
*.a
1616
mkmf.log
17+
/vendor/

lib/fluent/plugin/in_prometheus.rb

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,25 @@ def multi_workers_ready?
6767
def start
6868
super
6969

70+
# Normalize bind address: strip brackets if present (for consistency)
71+
# Brackets are only for URI formatting, not for socket binding
72+
@bind = @bind[1..-2] if @bind.start_with?('[') && @bind.end_with?(']')
73+
7074
scheme = @secure ? 'https' : 'http'
71-
log.debug "listening prometheus http server on #{scheme}:://#{@bind}:#{@port}/#{@metrics_path} for worker#{fluentd_worker_id}"
75+
# Format bind address properly for URLs (add brackets for IPv6)
76+
bind_display = @bind.include?(':') ? "[#{@bind}]" : @bind
77+
log.debug "listening prometheus http server on #{scheme}://#{bind_display}:#{@port}/#{@metrics_path} for worker#{fluentd_worker_id}"
7278

7379
proto = @secure ? :tls : :tcp
7480

75-
if @ssl && @ssl['enable'] && @ssl['extra_conf']
81+
# IPv6 + TLS combination is not currently supported
82+
if @bind.include?(':') && @secure
83+
raise Fluent::ConfigError, 'IPv6 with <transport tls> is not currently supported. Use bind 0.0.0.0 with TLS, or bind ::1 without TLS.'
84+
end
85+
86+
# Use webrick for IPv6 or SSL extra_conf
87+
# The http_server helper has issues with IPv6 URI construction
88+
if (@ssl && @ssl['enable'] && @ssl['extra_conf']) || @bind.include?(':')
7689
start_webrick
7790
return
7891
end
@@ -110,6 +123,8 @@ def start
110123
ssl_config
111124
end
112125

126+
# Use raw bind address for socket binding (no brackets)
127+
# Brackets are only for URL/URI formatting, not for bind()
113128
http_server_create_http_server(:in_prometheus_server, addr: @bind, port: @port, logger: log, proto: proto, tls_opts: tls_opt) do |server|
114129
server.get(@metrics_path) { |_req| all_metrics }
115130
server.get(@aggregated_metrics_path) { |_req| all_workers_metrics }
@@ -127,6 +142,7 @@ def shutdown
127142
private
128143

129144
# For compatiblity because http helper can't support extra_conf option
145+
# Also used for IPv6 addresses since http helper has IPv6 URI issues
130146
def start_webrick
131147
require 'webrick/https'
132148
require 'webrick'
@@ -138,28 +154,32 @@ def start_webrick
138154
Logger: WEBrick::Log.new(STDERR, WEBrick::Log::FATAL),
139155
AccessLog: [],
140156
}
141-
if (@ssl['certificate_path'] && @ssl['private_key_path'].nil?) || (@ssl['certificate_path'].nil? && @ssl['private_key_path'])
142-
raise RuntimeError.new("certificate_path and private_key_path most both be defined")
143-
end
157+
158+
# Configure SSL if enabled
159+
if @ssl && @ssl['enable']
160+
if (@ssl['certificate_path'] && @ssl['private_key_path'].nil?) || (@ssl['certificate_path'].nil? && @ssl['private_key_path'])
161+
raise RuntimeError.new("certificate_path and private_key_path most both be defined")
162+
end
144163

145-
ssl_config = {
146-
SSLEnable: true,
147-
SSLCertName: [['CN', 'nobody'], ['DC', 'example']]
148-
}
164+
ssl_config = {
165+
SSLEnable: true,
166+
SSLCertName: [['CN', 'nobody'], ['DC', 'example']]
167+
}
149168

150-
if @ssl['certificate_path']
151-
cert = OpenSSL::X509::Certificate.new(File.read(@ssl['certificate_path']))
152-
ssl_config[:SSLCertificate] = cert
153-
end
169+
if @ssl['certificate_path']
170+
cert = OpenSSL::X509::Certificate.new(File.read(@ssl['certificate_path']))
171+
ssl_config[:SSLCertificate] = cert
172+
end
154173

155-
if @ssl['private_key_path']
156-
key = OpenSSL::PKey.read(@ssl['private_key_path'])
157-
ssl_config[:SSLPrivateKey] = key
158-
end
174+
if @ssl['private_key_path']
175+
key = OpenSSL::PKey.read(@ssl['private_key_path'])
176+
ssl_config[:SSLPrivateKey] = key
177+
end
159178

160-
ssl_config[:SSLCACertificateFile] = @ssl['ca_path'] if @ssl['ca_path']
161-
ssl_config = ssl_config.merge(@ssl['extra_conf']) if @ssl['extra_conf']
162-
config = ssl_config.merge(config)
179+
ssl_config[:SSLCACertificateFile] = @ssl['ca_path'] if @ssl['ca_path']
180+
ssl_config = ssl_config.merge(@ssl['extra_conf']) if @ssl['extra_conf']
181+
config = ssl_config.merge(config)
182+
end
163183

164184
@log.on_debug do
165185
@log.debug("WEBrick conf: #{config}")
@@ -207,7 +227,16 @@ def all_workers_metrics
207227
end
208228

209229
def send_request_to_each_worker
210-
bind = (@bind == '0.0.0.0') ? '127.0.0.1' : @bind
230+
# Convert bind address to localhost for inter-worker communication
231+
# 0.0.0.0 and :: are not connectable, use localhost instead
232+
bind = case @bind
233+
when '0.0.0.0'
234+
'127.0.0.1'
235+
when '::'
236+
'::1' # IPv6 localhost
237+
else
238+
@bind
239+
end
211240
[*(@base_port...(@base_port + @num_workers))].each do |worker_port|
212241
do_request(host: bind, port: worker_port, secure: @secure) do |http|
213242
yield(http.get(@metrics_path))

lib/fluent/plugin/in_prometheus/async_wrapper.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@ module Fluent::Plugin
44
class PrometheusInput
55
module AsyncWrapper
66
def do_request(host:, port:, secure:)
7+
# Format host for URI - bracket IPv6 addresses if not already bracketed
8+
uri_host = if host.include?(':') && !host.start_with?('[')
9+
"[#{host}]"
10+
else
11+
host
12+
end
13+
714
endpoint =
815
if secure
916
context = OpenSSL::SSL::SSLContext.new
1017
context.verify_mode = OpenSSL::SSL::VERIFY_NONE
11-
Async::HTTP::Endpoint.parse("https://#{host}:#{port}", ssl_context: context)
18+
Async::HTTP::Endpoint.parse("https://#{uri_host}:#{port}", ssl_context: context)
1219
else
13-
Async::HTTP::Endpoint.parse("http://#{host}:#{port}")
20+
Async::HTTP::Endpoint.parse("http://#{uri_host}:#{port}")
1421
end
1522

1623
Async::HTTP::Client.open(endpoint) do |client|

spec/fluent/plugin/in_prometheus_spec.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,22 @@
8989
end
9090
end
9191

92+
context 'IPv6 with TLS' do
93+
let(:config) do
94+
%[
95+
@type prometheus
96+
bind ::1
97+
<transport tls>
98+
insecure true
99+
</transport>
100+
]
101+
end
102+
103+
it 'raises ConfigError for unsupported combination' do
104+
expect { driver.run(timeout: 1) }.to raise_error(Fluent::ConfigError, /IPv6 with <transport tls> is not currently supported/)
105+
end
106+
end
107+
92108
context 'old parameters are given' do
93109
context 'when extra_conf is used' do
94110
let(:config) do
@@ -278,4 +294,41 @@
278294
end
279295
end
280296
end
297+
298+
describe '#run with IPv6' do
299+
shared_examples 'IPv6 server binding' do |bind_addr, connect_addr, description|
300+
let(:config) do
301+
# Quote the bind address if it contains brackets
302+
bind_value = bind_addr.include?('[') ? "\"#{bind_addr}\"" : bind_addr
303+
%[
304+
@type prometheus
305+
bind #{bind_value}
306+
]
307+
end
308+
309+
it description do
310+
skip 'IPv6 not available on this system' unless ipv6_enabled?
311+
312+
driver.run(timeout: 3) do
313+
Net::HTTP.start(connect_addr, port) do |http|
314+
req = Net::HTTP::Get.new('/metrics')
315+
res = http.request(req)
316+
expect(res.code).to eq('200')
317+
end
318+
end
319+
end
320+
end
321+
322+
context 'IPv6 loopback address ::1' do
323+
include_examples 'IPv6 server binding', '::1', '::1', 'binds and serves on IPv6 loopback address'
324+
end
325+
326+
context 'IPv6 any address ::' do
327+
include_examples 'IPv6 server binding', '::', '::1', 'binds on :: and connects via ::1'
328+
end
329+
330+
context 'pre-bracketed IPv6 address [::1]' do
331+
include_examples 'IPv6 server binding', '[::1]', '::1', 'handles pre-bracketed address correctly'
332+
end
333+
end
281334
end

spec/spec_helper.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,21 @@
88

99
Fluent::Test.setup
1010
include Fluent::Test::Helpers
11+
12+
def ipv6_enabled?
13+
require 'socket'
14+
15+
begin
16+
# Try to actually bind to an IPv6 address to verify it works
17+
sock = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM, 0)
18+
sock.bind(Socket.sockaddr_in(0, '::1'))
19+
sock.close
20+
21+
# Also test that we can resolve IPv6 addresses
22+
# This is needed because some systems can bind but can't connect
23+
Socket.getaddrinfo('::1', nil, Socket::AF_INET6)
24+
true
25+
rescue Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, SocketError
26+
false
27+
end
28+
end

0 commit comments

Comments
 (0)