diff --git a/lib/datadog/core/utils/network.rb b/lib/datadog/core/utils/network.rb index af859e24d62..a3cc2403407 100644 --- a/lib/datadog/core/utils/network.rb +++ b/lib/datadog/core/utils/network.rb @@ -13,6 +13,7 @@ module Network true-client-ip x-client-ip x-forwarded + forwarded forwarded-for x-cluster-client-ip fastly-client-ip @@ -73,6 +74,8 @@ def ip_header(headers, ip_headers_to_check) next unless value ips = value.split(',') + ips = process_forwarded_header_values(ips) if name == 'forwarded' + ips.each do |ip| parsed_ip = ip_to_ipaddr(ip.strip) @@ -83,6 +86,22 @@ def ip_header(headers, ip_headers_to_check) nil end + def process_forwarded_header_values(values) + values.each_with_object([]) do |value, acc| + value.downcase! + + value.split(';').each do |tuple_str| + tuple_str.strip! + next unless tuple_str.start_with?('for=') + + tuple_str.delete_prefix!('for=') + tuple_str.delete!('"') + + acc << tuple_str + end + end + end + # Returns whether the given value is more likely to be an IPv4 than an IPv6 address. # # This is done by checking if a dot (`'.'`) character appears before a colon (`':'`) in the value. diff --git a/sig/datadog/core/utils/network.rbs b/sig/datadog/core/utils/network.rbs index d9be2618ed6..4ddc45cb08e 100644 --- a/sig/datadog/core/utils/network.rbs +++ b/sig/datadog/core/utils/network.rbs @@ -10,6 +10,7 @@ module Datadog private def self.ip_header: (Datadog::Core::HeaderCollection headers, ::Array[::String] ip_headers_to_check) -> untyped? + def self.process_forwarded_header_values: (::Array[::String] values) -> ::Array[::String] def self.strip_zone_specifier: (::String ipv6) -> ::String def self.strip_ipv6_port: (::String ip) -> ::String def self.strip_ipv4_port: (::String ip) -> ::String diff --git a/spec/datadog/core/utils/network_spec.rb b/spec/datadog/core/utils/network_spec.rb index 6b4adb0342c..a8e99663049 100644 --- a/spec/datadog/core/utils/network_spec.rb +++ b/spec/datadog/core/utils/network_spec.rb @@ -27,6 +27,45 @@ end end + context 'with Forwaded header' do + it 'correctly parses a single for IP' do + headers = Datadog::Core::HeaderCollection.from_hash({'Forwarded' => 'for=43.43.43.43;proto=http;by=203.0.113.43'}) + + result = described_class.stripped_ip_from_request_headers(headers) + expect(result).to eq('43.43.43.43') + end + + it 'is case-insencitive to keys in the header' do + headers = Datadog::Core::HeaderCollection.from_hash({'Forwarded' => 'For=43.43.43.43; Proto=http; By=203.0.113.43'}) + + result = described_class.stripped_ip_from_request_headers(headers) + expect(result).to eq('43.43.43.43') + end + + it 'correctly parses multiple for IPs' do + headers = Datadog::Core::HeaderCollection.from_hash( + {'Forwarded' => 'for=127.0.0.1;host="example.host";by=2.2.2.2;proto=http,for="1.1.1.1:6543"'} + ) + + result = described_class.stripped_ip_from_request_headers(headers) + expect(result).to eq('1.1.1.1') + end + + it 'correctly parses IPv6' do + headers = Datadog::Core::HeaderCollection.from_hash({'Forwarded' => 'for="[2001:db8:cafe::17]:4711"'}) + + result = described_class.stripped_ip_from_request_headers(headers) + expect(result).to eq('2001:db8:cafe::17') + end + + it 'returns nil for invalid values' do + headers = Datadog::Core::HeaderCollection.from_hash({'Forwarded' => 'foobar'}) + + result = described_class.stripped_ip_from_request_headers(headers) + expect(result).to be_nil + end + end + context 'with custom header value' do it 'returns the IP value if valid public address' do headers = Datadog::Core::HeaderCollection.from_hash(