From 85ee26ecc9eeb1a3342dbc0cd8d6bc268b0a50f1 Mon Sep 17 00:00:00 2001 From: Gustavo Caso Date: Thu, 2 Mar 2023 18:12:03 +0100 Subject: [PATCH] Update the client IP extraction resolution base on the latest RFC. - Update the list of HTTP headers to look for IP - Add support to parsing HTTP IP Headers with multiple IP addresses - Update specs - Change Tracing::ClientIp module methods visibility --- LICENSE-3rdparty.csv | 1 + .../appsec/contrib/rack/gateway/request.rb | 13 +- lib/datadog/core/utils/network.rb | 140 +++++++++++++ lib/datadog/core/vendor/ipaddr.rb | 78 +++++++ lib/datadog/tracing/client_ip.rb | 179 ++++------------ sig/datadog/core/header_collection.rbs | 14 ++ sig/datadog/core/utils/network.rbs | 23 +++ sig/datadog/tracing/client_ip.rbs | 15 ++ spec/datadog/core/utils/network_spec.rb | 115 +++++++++++ spec/datadog/tracing/client_ip_spec.rb | 191 +++--------------- 10 files changed, 451 insertions(+), 318 deletions(-) create mode 100644 lib/datadog/core/utils/network.rb create mode 100644 lib/datadog/core/vendor/ipaddr.rb create mode 100644 sig/datadog/core/header_collection.rbs create mode 100644 sig/datadog/core/utils/network.rbs create mode 100644 sig/datadog/tracing/client_ip.rbs create mode 100644 spec/datadog/core/utils/network_spec.rb diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index fb0d3efa286..7002cbb53f6 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -5,3 +5,4 @@ lib/datadog/tracing/contrib/utils/quantization/http.rb,https://github.com/ruby/u ext/ddtrace_profiling_native_extension/private_vm_api_access,https://github.com/ruby/ruby,BSD-2-Clause,"Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved." msgpack,https://rubygems.org/gems/msgpack,Apache-2.0,"Copyright (c) 2008-2015 Sadayuki Furuhashi" debase-ruby_core_source,https://rubygems.org/gems/debase-ruby_core_source,MIT for gem and BSD-2-Clause for Ruby sources,"Copyright (c) 2012 Gabriel Horner. Files from Ruby sources are Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved." +lib/datadog/core/vendor/ipaddr,https://github.com/ruby/ipaddr/blob/master/lib/ipaddr.rb,BSD 2-Clause "Simplified" License,"Copyright (c) 2002 Hajimu UMEMOTO Copyright (c) 2007-2017 Akinori MUSHA " diff --git a/lib/datadog/appsec/contrib/rack/gateway/request.rb b/lib/datadog/appsec/contrib/rack/gateway/request.rb index 31d6c8a45e2..1421ac40af9 100644 --- a/lib/datadog/appsec/contrib/rack/gateway/request.rb +++ b/lib/datadog/appsec/contrib/rack/gateway/request.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true require_relative '../../../instrumentation/gateway/argument' +require_relative '../../../../core/header_collection' require_relative '../../../../tracing/client_ip' -require_relative '../../../../tracing/contrib/rack/header_collection' module Datadog module AppSec @@ -85,16 +85,9 @@ def form_hash def client_ip remote_ip = remote_addr - headers = Datadog::Tracing::Contrib::Rack::Header::RequestHeaderCollection.new(env) + header_collection = Datadog::Core::HeaderCollection.from_hash(headers) - result = Datadog::Tracing::ClientIp.raw_ip_from_request(headers, remote_ip) - - if result.raw_ip - ip = Datadog::Tracing::ClientIp.strip_decorations(result.raw_ip) - return unless Datadog::Tracing::ClientIp.valid_ip?(ip) - - ip - end + Datadog::Tracing::ClientIp.extract_client_ip(header_collection, remote_ip) end end end diff --git a/lib/datadog/core/utils/network.rb b/lib/datadog/core/utils/network.rb new file mode 100644 index 00000000000..6bf55359799 --- /dev/null +++ b/lib/datadog/core/utils/network.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require 'ipaddr' +require_relative '../vendor/ipaddr' + +module Datadog + module Core + module Utils + # Common Network utility functions. + module Network + DEFAULT_IP_HEADERS_NAMES = %w[ + x-forwarded-for + x-real-ip + true-client-ip + x-client-ip + x-forwarded + forwarded-for + x-cluster-client-ip + fastly-client-ip + cf-connecting-ip + cf-connecting-ipv6 + ].freeze + + class << self + # Returns a client IP associated with the request if it was + # retrieved successfully. + # + # + # @param [Datadog::Core::HeaderCollection, #get, nil] headers The request headers + # @param [Array] list of headers to check. + # @return [String] IP value without the port and the zone indentifier. + # @return [nil] when no valid IP value found. + def stripped_ip_from_request_headers(headers, ip_headers_to_check: DEFAULT_IP_HEADERS_NAMES) + ip = ip_header(headers, ip_headers_to_check) + + ip ? ip.to_s : nil + end + + # @param [String] IP value. + # @return [String] IP value without the port and the zone indentifier. + # @return [nil] when no valid IP value found. + def stripped_ip(ip) + ip = ip_to_ipaddr(ip) + ip ? ip.to_s : nil + end + + private + + # @param [String] IP value. + # @return [IPaddr] + # @return [nil] when no valid IP value found. + def ip_to_ipaddr(ip) + return unless ip + + clean_ip = if likely_ipv4?(ip) + strip_ipv4_port(ip) + else + strip_zone_specifier(strip_ipv6_port(ip)) + end + + begin + IPAddr.new(clean_ip) + rescue IPAddr::Error + nil + end + end + + def ip_header(headers, ip_headers_to_check) + return unless headers + + ip_headers_to_check.each do |name| + value = headers.get(name) + + next unless value + + ips = value.split(',') + ips.each do |ip| + parsed_ip = ip_to_ipaddr(ip.strip) + + return parsed_ip if global_ip?(parsed_ip) + end + end + + nil + 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. + # The rationale is that in valid IPv6 addresses, colons will always preced dots, + # and in valid IPv4 addresses dots will always preced colons. + def likely_ipv4?(value) + dot_index = value.index('.') || value.size + colon_index = value.index(':') || value.size + + dot_index < colon_index + end + + def strip_zone_specifier(ipv6) + ipv6.gsub(/%.*/, '') + end + + def strip_ipv6_port(ip) + if /\[(.*)\](?::\d+)?/ =~ ip + Regexp.last_match(1) + else + ip + end + end + + def strip_ipv4_port(ip) + ip.gsub(/:\d+\z/, '') + end + + def global_ip?(parsed_ip) + parsed_ip && !private?(parsed_ip) && !loopback?(parsed_ip) && !link_local?(parsed_ip) + end + + # TODO: remove once we drop support for ruby 2.1, 2.2, 2.3, 2.4 + # replace with ip.private? + def private?(ip) + Datadog::Core::Vendor::IPAddr.private?(ip) + end + + # TODO: remove once we drop support for ruby 2.1, 2.2, 2.3, 2.4 + # replace with ip.link_local? + def link_local?(ip) + Datadog::Core::Vendor::IPAddr.link_local?(ip) + end + + # TODO: remove once we drop support for ruby 2.1, 2.2, 2.3, 2.4 + # replace with ip.loopback + def loopback?(ip) + Datadog::Core::Vendor::IPAddr.loopback?(ip) + end + end + end + end + end +end diff --git a/lib/datadog/core/vendor/ipaddr.rb b/lib/datadog/core/vendor/ipaddr.rb new file mode 100644 index 00000000000..d5faeaee492 --- /dev/null +++ b/lib/datadog/core/vendor/ipaddr.rb @@ -0,0 +1,78 @@ +# Copyright (c) 2002 Hajimu UMEMOTO +# Copyright (c) 2007-2017 Akinori MUSHA + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +module Datadog + module Core + module Vendor + # vendor code from https://github.com/ruby/ipaddr/blob/master/lib/ipaddr.rb + # Ruby version below 2.5 does not have the IpAddr#private? method + # We have to vendor the code because ruby versions below 2.5 did not extract ipaddr as a gem + # So we can not specify a specific version for ipaddr for ruby versions: 2.1, 2.2, 2.3, 2.4 + module IPAddr + class << self + def private?(ip) + addr = ip.instance_variable_get(:@addr) + + case ip.family + when Socket::AF_INET + addr & 0xff000000 == 0x0a000000 || # 10.0.0.0/8 + addr & 0xfff00000 == 0xac100000 || # 172.16.0.0/12 + addr & 0xffff0000 == 0xc0a80000 # 192.168.0.0/16 + when Socket::AF_INET6 + addr & 0xfe00_0000_0000_0000_0000_0000_0000_0000 == 0xfc00_0000_0000_0000_0000_0000_0000_0000 + else + raise IPAddr::AddressFamilyError, 'unsupported address family' + end + end + + def link_local?(ip) + addr = ip.instance_variable_get(:@addr) + + case ip.family + when Socket::AF_INET + addr & 0xffff0000 == 0xa9fe0000 # 169.254.0.0/16 + when Socket::AF_INET6 + addr & 0xffc0_0000_0000_0000_0000_0000_0000_0000 == 0xfe80_0000_0000_0000_0000_0000_0000_0000 + else + raise IPAddr::AddressFamilyError, 'unsupported address family' + end + end + + def loopback?(ip) + addr = ip.instance_variable_get(:@addr) + + case ip.family + when Socket::AF_INET + addr & 0xff000000 == 0x7f000000 + when Socket::AF_INET6 + addr == 1 + else + raise IPAddr::AddressFamilyError, 'unsupported address family' + end + end + end + end + end + end +end diff --git a/lib/datadog/tracing/client_ip.rb b/lib/datadog/tracing/client_ip.rb index a2e5b0fdad7..3829a8b94cb 100644 --- a/lib/datadog/tracing/client_ip.rb +++ b/lib/datadog/tracing/client_ip.rb @@ -1,162 +1,61 @@ +# frozen_string_literal: true + require_relative '../core/configuration' +require_relative '../core/utils/network' require_relative 'metadata/ext' require_relative 'span' -require 'ipaddr' - module Datadog module Tracing # Common functions for supporting the `http.client_ip` span attribute. module ClientIp - DEFAULT_IP_HEADERS_NAMES = %w[ - x-forwarded-for - x-real-ip - x-client-ip - x-forwarded - x-cluster-client-ip - forwarded-for - forwarded - via - true-client-ip - ].freeze - - TAG_MULTIPLE_IP_HEADERS = '_dd.multiple-ip-headers'.freeze - - # Sets the `http.client_ip` tag on the given span. - # - # This function respects the user's settings: if they disable the client IP tagging, - # or provide a different IP header name. - # - # If multiple IP headers are present in the request, this function will instead set - # the `_dd.multiple-ip-headers` tag with the names of the present headers, - # and **NOT** set the `http.client_ip` tag. - # - # @param [Span] span The span that's associated with the request. - # @param [HeaderCollection, #get, nil] headers A collection with the request headers. - # @param [String, nil] remote_ip The remote IP the request associated with the span is sent to. - def self.set_client_ip_tag(span, headers: nil, remote_ip: nil) - return unless configuration.enabled - - set_client_ip_tag!(span, headers: headers, remote_ip: remote_ip) - end - - # Forcefully sets the `http.client_ip` tag on the given span. - # - # This function ignores the user's `enabled` setting. - # - # @param [Span] span The span that's associated with the request. - # @param [HeaderCollection, #get, nil] headers A collection with the request headers. - # @param [String, nil] remote_ip The remote IP the request associated with the span is sent to. - def self.set_client_ip_tag!(span, headers: nil, remote_ip: nil) - result = raw_ip_from_request(headers, remote_ip) - - if result.raw_ip - ip = strip_decorations(result.raw_ip) - return unless valid_ip?(ip) - - span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, ip) - elsif result.multiple_ip_headers - span.set_tag(TAG_MULTIPLE_IP_HEADERS, result.multiple_ip_headers.keys.join(',')) + class << self + # Sets the `http.client_ip` tag on the given span. + # + # This function respects the user's settings: if they disable the client IP tagging, + # or provide a different IP header name. + # + # @param [Span] span The span that's associated with the request. + # @param [HeaderCollection, #get, nil] headers A collection with the request headers. + # @param [String, nil] remote_ip The remote IP the request associated with the span is sent to. + def set_client_ip_tag(span, headers: nil, remote_ip: nil) + return unless configuration.enabled + + set_client_ip_tag!(span, headers: headers, remote_ip: remote_ip) end - end - - IpExtractionResult = Struct.new(:raw_ip, :multiple_ip_headers) - - # Returns a result struct that holds the raw client IP associated with the request if it was - # retrieved successfully. - # - # The client IP is looked up by the following logic: - # * If the user has configured a header name, return that header's value. - # * If exactly one of the known IP headers is present, return that header's value. - # * If none of the known IP headers are present, return the remote IP from the request. - # - # If more than one of the known IP headers is present, the result will have a `multiple_ip_headers` - # field with the name of the present IP headers. - # - # @param [Datadog::Core::HeaderCollection, #get, nil] headers The request headers - # @param [String] remote_ip The remote IP of the request. - # @return [IpExtractionResult] A struct that holds the unprocessed IP value, - # or `nil` if it wasn't found. Additionally, the `multiple_ip_headers` fields will hold the - # name of known IP headers present in the request if more than one of these were found. - def self.raw_ip_from_request(headers, remote_ip) - return IpExtractionResult.new(headers && headers.get(configuration.header_name), nil) if configuration.header_name - headers_present = ip_headers(headers) - - case headers_present.size - when 0 - IpExtractionResult.new(remote_ip, nil) - when 1 - IpExtractionResult.new(headers_present.values.first, nil) - else - IpExtractionResult.new(nil, headers_present) + # Forcefully sets the `http.client_ip` tag on the given span. + # + # This function ignores the user's `enabled` setting. + # + # @param [Span] span The span that's associated with the request. + # @param [HeaderCollection, #get, nil] headers A collection with the request headers. + # @param [String, nil] remote_ip The remote IP the request associated with the span is sent to. + def set_client_ip_tag!(span, headers: nil, remote_ip: nil) + ip = extract_client_ip(headers, remote_ip) + + span.set_tag(Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, ip) if ip end - end - # Removes any port notations or zone specifiers from the IP address without - # verifying its validity. - def self.strip_decorations(address) - return strip_ipv4_port(address) if likely_ipv4?(address) + def extract_client_ip(headers, remote_ip) + if headers && configuration.header_name + return Datadog::Core::Utils::Network.stripped_ip_from_request_headers( + headers, + ip_headers_to_check: Array(configuration.header_name) + ) + end - address = strip_ipv6_port(address) - - strip_zone_specifier(address) - end + ip_from_headers = Datadog::Core::Utils::Network.stripped_ip_from_request_headers(headers) if headers - def self.strip_zone_specifier(ipv6) - ipv6.gsub(/%.*/, '') - end - - def self.strip_ipv4_port(ip) - ip.gsub(/:\d+\z/, '') - end - - def self.strip_ipv6_port(ip) - if /\[(.*)\](?::\d+)?/ =~ ip - Regexp.last_match(1) - else - ip + ip_from_headers || Datadog::Core::Utils::Network.stripped_ip(remote_ip) 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. - # The rationale is that in valid IPv6 addresses, colons will always preced dots, - # and in valid IPv4 addresses dots will always preced colons. - def self.likely_ipv4?(value) - dot_index = value.index('.') || value.size - colon_index = value.index(':') || value.size - - dot_index < colon_index - end - # Determines whether the given string is a valid IPv4 or IPv6 address. - def self.valid_ip?(ip) - # Client IPs should not have subnet masks even though IPAddr can parse them. - return false if ip.include?('/') + private - begin - IPAddr.new(ip) - - true - rescue IPAddr::Error - false + def configuration + Datadog.configuration.tracing.client_ip end end - - def self.ip_headers(headers) - return {} unless headers - - DEFAULT_IP_HEADERS_NAMES.each_with_object({}) do |name, result| - value = headers.get(name) - result[name] = value unless value.nil? - end - end - - def self.configuration - Datadog.configuration.tracing.client_ip - end end end end diff --git a/sig/datadog/core/header_collection.rbs b/sig/datadog/core/header_collection.rbs new file mode 100644 index 00000000000..4ac613a0927 --- /dev/null +++ b/sig/datadog/core/header_collection.rbs @@ -0,0 +1,14 @@ +module Datadog + module Core + class HeaderCollection + def get: (::String header_name) -> nil + def self.from_hash: (untyped hash) -> HashHeaderCollection + end + + class HashHeaderCollection < HeaderCollection + def initialize: (untyped hash) -> void + + def get: (::String header_name) -> ::String? + end + end +end diff --git a/sig/datadog/core/utils/network.rbs b/sig/datadog/core/utils/network.rbs new file mode 100644 index 00000000000..d9be2618ed6 --- /dev/null +++ b/sig/datadog/core/utils/network.rbs @@ -0,0 +1,23 @@ +module Datadog + module Core + module Utils + module Network + DEFAULT_IP_HEADERS_NAMES: ::Array[::String] + + def self.stripped_ip_from_request_headers: (Datadog::Core::HeaderCollection headers, ?::Array[::String] ip_headers_to_check) -> ::String? + def self.stripped_ip: (::String ip) -> String? + + private + + def self.ip_header: (Datadog::Core::HeaderCollection headers, ::Array[::String] ip_headers_to_check) -> untyped? + def self.strip_zone_specifier: (::String ipv6) -> ::String + def self.strip_ipv6_port: (::String ip) -> ::String + def self.strip_ipv4_port: (::String ip) -> ::String + def self.global_ip?: (untyped parsed_ip) -> bool + def self.private?: (untyped ip) -> bool + def self.link_local?: (untyped ip) -> bool + def self.loopback?: (untyped ip) -> bool + end + end + end +end diff --git a/sig/datadog/tracing/client_ip.rbs b/sig/datadog/tracing/client_ip.rbs new file mode 100644 index 00000000000..da088a7f3db --- /dev/null +++ b/sig/datadog/tracing/client_ip.rbs @@ -0,0 +1,15 @@ +module Datadog + module Tracing + module ClientIp + def self.set_client_ip_tag: (Span span, ?headers: Core::HeaderCollection?, ?remote_ip: String?) -> void + + def self.set_client_ip_tag!: (Span span, ?headers: Core::HeaderCollection?, ?remote_ip: String?) -> void + + def self.extract_client_ip: (Core::HeaderCollection? headers, String? remote_ip) -> String? + + private + + def self.configuration: () -> Core::Configuration + end + end +end diff --git a/spec/datadog/core/utils/network_spec.rb b/spec/datadog/core/utils/network_spec.rb new file mode 100644 index 00000000000..7d50cbb0a8b --- /dev/null +++ b/spec/datadog/core/utils/network_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +require 'datadog/core/utils/network' + +RSpec.describe Datadog::Core::Utils::Network do + describe '.stripped_ip_from_request_headers' do + context 'with default IP headers' do + context 'iterates over the default headers (DEFAULT_IP_HEADERS_NAMES) in order' do + it 'returns the first valid public IP value' do + headers = Datadog::Core::HeaderCollection.from_hash( + { 'X-Forwarded-For' => '10.42.42.42', + 'True-Client-Ip' => '43.43.43.43', + 'X-Cluster-Client-Ip' => '10.0.0.1', } + ) + + result = described_class.stripped_ip_from_request_headers(headers) + expect(result).to eq('43.43.43.43') + end + end + + context 'multiple IP addresses present in the header' do + it 'returns the first valid public IP address' do + headers = Datadog::Core::HeaderCollection.from_hash({ 'X-Forwarded-For' => '10.42.42.42,43.43.43.43,fe80::1' }) + + result = described_class.stripped_ip_from_request_headers(headers) + expect(result).to eq('43.43.43.43') + end + end + + context 'with custom header value' do + it 'returns the IP value if valid public address' do + headers = Datadog::Core::HeaderCollection.from_hash( + { + 'X-Forwarded-For' => '64.233.161.147', + 'test-header' => '43.43.43.43', + } + ) + + result = described_class.stripped_ip_from_request_headers(headers, ip_headers_to_check: ['test-header']) + expect(result).to eq('43.43.43.43') + end + + it 'returns nil if header not present' do + headers = Datadog::Core::HeaderCollection.from_hash({}) + + result = described_class.stripped_ip_from_request_headers(headers, ip_headers_to_check: ['test-header']) + expect(result).to be_nil + end + + it 'returns nil if header value is not valid' do + headers = Datadog::Core::HeaderCollection.from_hash({ 'test-header' => 'dd' }) + + result = described_class.stripped_ip_from_request_headers(headers, ip_headers_to_check: ['test-header']) + expect(result).to be_nil + end + end + + it 'returns nil if no public valid IP addresss present in the headers' do + headers = Datadog::Core::HeaderCollection.from_hash( + { 'X-Forwarded-For' => '10.42.42.42' } + ) + + result = described_class.stripped_ip_from_request_headers(headers) + expect(result).to be_nil + end + end + end + + describe '.stripped_ip' do + context 'valid IP' do + it 'returns the IP value, with port and zone identifier removed' do + ips = + [ + ['10.0.0.0', '10.0.0.0'], + ['10.0.0.1', '10.0.0.1'], + ['10.0.0.1:8080', '10.0.0.1'], + ['1080:0000:0000:0000:0008:0800:200C:417A', '1080::8:800:200c:417a'], + ['1080:0:0:0:8:800:200C:417A', '1080::8:800:200c:417a'], + ['1080:0::8:800:200C:417A', '1080::8:800:200c:417a'], + ['1080::8:800:200C:417A', '1080::8:800:200c:417a'], + ['FF01:0:0::0:0:43', 'ff01::43'], + ['FF01::43', 'ff01::43'], + ['fe80::208:74ff:feda:625c', 'fe80::208:74ff:feda:625c'], + ['fe80::208:74ff:feda:625c%eth0', 'fe80::208:74ff:feda:625c'], + ['ff80:03:02:01::', 'ff80:3:2:1::'], + ['[fe80::208:74ff:feda:625c]', 'fe80::208:74ff:feda:625c'], + ['[fe80::208:74ff:feda:625c]:8080', 'fe80::208:74ff:feda:625c'], + ['[fe80::208:74ff:feda:625c%eth0]:8080', 'fe80::208:74ff:feda:625c'], + ] + + ips.each do |ip, expected_result| + result = described_class.stripped_ip(ip) + expect(result).to eq(expected_result) + end + end + end + + context 'invalid IP' do + it 'returns nil' do + ips = + [ + '', + 'dd', + '02001:0000:1234:0000:0000:C1C0:ABCD:0876', + '2001:0000:1234:0000:00001:C1C0:ABCD:0876' + ] + + ips.each do |ip| + result = described_class.stripped_ip(ip) + expect(result).to be_nil + end + end + end + end +end diff --git a/spec/datadog/tracing/client_ip_spec.rb b/spec/datadog/tracing/client_ip_spec.rb index cb4faf58d6f..619e9f0391f 100644 --- a/spec/datadog/tracing/client_ip_spec.rb +++ b/spec/datadog/tracing/client_ip_spec.rb @@ -20,82 +20,6 @@ without_warnings { Datadog.configuration.reset! } end - describe '#valid_ip?' do - context 'when given valid ip addresses' do - ips = - [ - '10.0.0.0', - '10.0.0.1', - '10.0.0.1:8080', - 'FEDC:BA98:7654:3210:FEDC:BA98:7654:3210', - '1080:0000:0000:0000:0008:0800:200C:417A', - '1080:0:0:0:8:800:200C:417A', - '1080:0::8:800:200C:417A', - '1080::8:800:200C:417A', - 'FF01:0:0:0:0:0:0:43', - 'FF01:0:0::0:0:43', - 'FF01::43', - '0:0:0:0:0:0:0:1', - '0:0:0::0:0:1', - '::1', - '0:0:0:0:0:0:0:0', - '0:0:0::0:0:0', - '::', - 'fe80::208:74ff:feda:625c', - 'fe80::208:74ff:feda:625c%eth0', - 'ff80:03:02:01::', - '[fe80::208:74ff:feda:625c]', - '[fe80::208:74ff:feda:625c]:8080', - '[fe80::208:74ff:feda:625c%eth0]:8080' - ] - - ips.each do |ip| - subject do - normalized = client_ip.strip_decorations(ip) - - client_ip.valid_ip?(normalized) - end - - it "returns true for #{ip}" do - is_expected.to be true - end - end - end - - context 'when given invalid ip addresses' do - ips = - [ - '', - '10.0.0.256', - '10.0.0.0.0', - '10.0.0', - '10.0', - '0.0.0.0/0', - '10.0.0.1/24', - '10.0.0.1/255.255.255.0', - ':1:2:3:4:5:6:7', - ':1:2:3:4:5:6:7', - '2002:516:2:200', - 'dd', - '2001:db8::8:800:200c:417a/64', - '02001:0000:1234:0000:0000:C1C0:ABCD:0876', - '2001:0000:1234:0000:00001:C1C0:ABCD:0876' - ] - - ips.each do |ip| - subject do - normalized = client_ip.strip_decorations(ip) - - client_ip.valid_ip?(normalized) - end - - it "returns false for #{ip}" do - is_expected.to be false - end - end - end - end - describe '#set_client_ip_tag' do let(:span) do instance_double('Span') @@ -106,7 +30,7 @@ it 'does nothing' do expect(span).to_not receive(:set_tag) - client_ip.set_client_ip_tag(span, remote_ip: '1.1.1.1') + client_ip.set_client_ip_tag(span, remote_ip: '15.173.99.139') end end @@ -117,16 +41,16 @@ end it 'ignores default header names' do - headers = Datadog::Core::HeaderCollection.from_hash({ 'X-Forwarded-For' => '1.1.1.1' }) + headers = Datadog::Core::HeaderCollection.from_hash({ 'X-Forwarded-For' => '15.173.99.139' }) expect(span).to_not receive(:set_tag) client_ip.set_client_ip_tag(span, headers: headers) end it 'uses custom header value as client ip' do - headers = Datadog::Core::HeaderCollection.from_hash({ 'My-Custom-Header' => '1.1.1.1' }) + headers = Datadog::Core::HeaderCollection.from_hash({ 'My-Custom-Header' => '15.173.99.139' }) - expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '1.1.1.1') + expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '15.173.99.139') client_ip.set_client_ip_tag(span, headers: headers) end @@ -157,22 +81,22 @@ ) expect(span).to_not receive(:set_tag) - client_ip.set_client_ip_tag(span, headers: headers, remote_ip: '1.1.1.1') + client_ip.set_client_ip_tag(span, headers: headers, remote_ip: '15.173.99.139') end it 'prefers ip from custom header over remote ip' do - headers = Datadog::Core::HeaderCollection.from_hash({ 'My-Custom-Header' => '1.1.1.1' }) + headers = Datadog::Core::HeaderCollection.from_hash({ 'My-Custom-Header' => '15.173.99.139' }) - expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '1.1.1.1') + expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '15.173.99.139') client_ip.set_client_ip_tag(span, headers: headers, remote_ip: '2.2.2.2') end end - context 'when single ip header is present' do + context 'when an ip header is present' do it 'uses value from header as client ip' do - headers = Datadog::Core::HeaderCollection.from_hash({ 'X-Forwarded-For' => '1.1.1.1' }) + headers = Datadog::Core::HeaderCollection.from_hash({ 'X-Forwarded-For' => '15.173.99.139' }) - expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '1.1.1.1') + expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '15.173.99.139') client_ip.set_client_ip_tag(span, headers: headers) end @@ -183,96 +107,27 @@ client_ip.set_client_ip_tag(span, headers: headers) end - it 'does not use remote ip if header value is not a valid ip' do + it 'uses remote ip if header value is not a valid ip' do headers = Datadog::Core::HeaderCollection.from_hash({ 'X-Forwarded-For' => '1.11.1' }) - expect(span).to_not receive(:set_tag) - client_ip.set_client_ip_tag(span, headers: headers, remote_ip: '1.1.1.1') + expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '15.173.99.139') + client_ip.set_client_ip_tag(span, headers: headers, remote_ip: '15.173.99.139') end it 'prefers ip from header over remote ip' do - headers = Datadog::Core::HeaderCollection.from_hash({ 'X-Forwarded-For' => '1.1.1.1' }) + headers = Datadog::Core::HeaderCollection.from_hash({ 'X-Forwarded-For' => '15.173.99.139' }) - expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '1.1.1.1') + expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '15.173.99.139') client_ip.set_client_ip_tag(span, headers: headers, remote_ip: '2.2.2.2') end end - context 'when multiple ip headers are present' do - it 'sets multiple ip headers tag only' do - headers = Datadog::Core::HeaderCollection.from_hash( - { - 'X-Forwarded-For' => '1.1.1.1', - 'X-Client-Ip' => '2.2.2.2' - } - ) - - expect(span).to_not receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, anything) - expect(span).to receive(:set_tag).with(client_ip::TAG_MULTIPLE_IP_HEADERS, 'x-forwarded-for,x-client-ip') - client_ip.set_client_ip_tag(span, headers: headers) - end - - it 'sets multiple ip headers tag only even if all ips are the same' do - headers = Datadog::Core::HeaderCollection.from_hash( - { - 'X-Forwarded-For' => '1.1.1.1', - 'X-Client-Ip' => '1.1.1.1' - } - ) - - expect(span).to_not receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, anything) - expect(span).to receive(:set_tag).with(client_ip::TAG_MULTIPLE_IP_HEADERS, 'x-forwarded-for,x-client-ip') - client_ip.set_client_ip_tag(span, headers: headers) - end - - it 'prefers multiple ip headers tag only over remote ip' do - headers = Datadog::Core::HeaderCollection.from_hash( - { - 'X-Forwarded-For' => '1.1.1.1', - 'X-Client-Ip' => '2.2.2.2' - } - ) - - expect(span).to_not receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, anything) - expect(span).to receive(:set_tag).with(client_ip::TAG_MULTIPLE_IP_HEADERS, 'x-forwarded-for,x-client-ip') - client_ip.set_client_ip_tag(span, headers: headers, remote_ip: '3.3.3.3') - end - - it 'includes ip headers with invalid ips in multiple ip headers tag' do - headers = Datadog::Core::HeaderCollection.from_hash( - { - 'X-Forwarded-For' => '1.1.1.1', - 'X-Client-Ip' => '2.2.2.2.3', - 'X-Real-Ip' => '3.3.3.3' - } - ) - - expect(span).to_not receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, anything) - expect(span).to receive(:set_tag).with(client_ip::TAG_MULTIPLE_IP_HEADERS, 'x-forwarded-for,x-real-ip,x-client-ip') - client_ip.set_client_ip_tag(span, headers: headers) - end - - it 'includes ip headers with invalid ips in multiple ip headers tag even if exactly one ip is valid' do - headers = Datadog::Core::HeaderCollection.from_hash( - { - 'X-Forwarded-For' => '1.1.1.1', - 'X-Client-Ip' => '2.22.2', - 'X-Real-Ip' => 'dd' - } - ) - - expect(span).to_not receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, anything) - expect(span).to receive(:set_tag).with(client_ip::TAG_MULTIPLE_IP_HEADERS, 'x-forwarded-for,x-real-ip,x-client-ip') - client_ip.set_client_ip_tag(span, headers: headers) - end - end - context 'when no ip headers are present' do let(:headers) { Datadog::Core::HeaderCollection.from_hash({}) } it 'uses remote ip as client ip as fallback' do - expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '1.1.1.1') - client_ip.set_client_ip_tag(span, headers: headers, remote_ip: '1.1.1.1') + expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '15.173.99.139') + client_ip.set_client_ip_tag(span, headers: headers, remote_ip: '15.173.99.139') end it 'does nothing if remote ip is invalid' do @@ -292,8 +147,8 @@ end it 'uses remote ip as client ip as fallback' do - expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '1.1.1.1') - client_ip.set_client_ip_tag(span, headers: headers, remote_ip: '1.1.1.1') + expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '15.173.99.139') + client_ip.set_client_ip_tag(span, headers: headers, remote_ip: '15.173.99.139') end it 'does nothing if remote ip is invalid' do @@ -304,8 +159,8 @@ context 'when ip' do it 'is plain ipv4' do - expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '1.1.1.1') - client_ip.set_client_ip_tag(span, remote_ip: '1.1.1.1') + expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '15.173.99.139') + client_ip.set_client_ip_tag(span, remote_ip: '15.173.99.139') end it 'is plain ipv6' do @@ -317,8 +172,8 @@ end it 'is ipv4 and port' do - expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '1.1.1.1') - client_ip.set_client_ip_tag(span, remote_ip: '1.1.1.1:8080') + expect(span).to receive(:set_tag).with(Datadog::Tracing::Metadata::Ext::HTTP::TAG_CLIENT_IP, '15.173.99.139') + client_ip.set_client_ip_tag(span, remote_ip: '15.173.99.139:8080') end it 'is ipv6 and port' do