From e53e782223b15cf7846d543197ac7da03d44fab7 Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Sat, 27 Dec 2025 11:56:19 +1100 Subject: [PATCH] Find supernets, don't create them --- app/models/network_range.rb | 27 -------------- app/models/waf_policy.rb | 70 ++++++++++++++++++++++--------------- 2 files changed, 41 insertions(+), 56 deletions(-) diff --git a/app/models/network_range.rb b/app/models/network_range.rb index a147a06..0c98316 100644 --- a/app/models/network_range.rb +++ b/app/models/network_range.rb @@ -136,33 +136,6 @@ class NetworkRange < ApplicationRecord .order("masklen(network) ASC") # Least specific child first end - # Find or create an ancestor network at a specific prefix length - # For example, given 192.168.1.0/24 and prefix 16, returns 192.168.0.0/16 - def find_or_create_ancestor_at_prefix(target_prefix) - return self if prefix_length <= target_prefix - - # Use PostgreSQL's set_masklen to create the ancestor CIDR - result = self.class.connection.execute( - "SELECT set_masklen('#{network}'::inet, #{target_prefix})::text as ancestor_cidr" - ).first - - return self unless result - - ancestor_cidr = result["ancestor_cidr"] - return self if ancestor_cidr == cidr - - # Find or create the ancestor network range - ancestor = NetworkRange.find_by(network: ancestor_cidr) - - if ancestor.nil? - # Create a virtual ancestor (not persisted, just for reference) - # The caller can decide whether to persist it - ancestor = NetworkRange.new(network: ancestor_cidr, source: 'inherited') - end - - ancestor - end - # Find nearest parent with intelligence data def parent_with_intelligence # Find all parent ranges (networks that contain this network) diff --git a/app/models/waf_policy.rb b/app/models/waf_policy.rb index 1c7d0bf..932e689 100644 --- a/app/models/waf_policy.rb +++ b/app/models/waf_policy.rb @@ -517,41 +517,53 @@ validate :targets_must_be_array country = network_range.country || network_range.inherited_intelligence[:country] return network_range unless country - # Walk up from current prefix to /8 (IPv4) or /32 to /1 (IPv6) - current_prefix = network_range.prefix_length - max_prefix = network_range.ipv4? ? 8 : 1 + # Check if this network has IPAPI data with a larger CIDR (asn.route or ipapi_returned_cidr) + ipapi_cidr = network_range.network_data&.dig('ipapi', 'asn', 'route') || + network_range.network_data&.dig('ipapi_returned_cidr') - current_prefix.step(-1, -1).each do |prefix| - break if prefix < max_prefix - - candidate = network_range.find_or_create_ancestor_at_prefix(prefix) - # Check if candidate has geo data (either direct or inherited) - candidate_country = candidate.country || (candidate.persisted? ? candidate.inherited_intelligence[:country] : nil) - - # For virtual (unpersisted) ancestors, we need to check if any existing records in that range have the country - if candidate_country.nil? && !candidate.persisted? - # Query database to see if networks in this range have the same country - country_check = NetworkRange.where("network <<= ?", candidate.cidr) - .where.not(country: nil) - .select(:country) - .distinct - # If all networks in this range have the same country, we can use it - if country_check.count == 1 && country_check.first.country == country - candidate_country = country + if ipapi_cidr && ipapi_cidr != network_range.cidr + # IPAPI returned a larger network - use it if it exists + existing = NetworkRange.find_by(network: ipapi_cidr) + if existing + existing_country = existing.country || existing.inherited_intelligence[:country] + if existing_country == country + Rails.logger.debug "Using IPAPI CIDR #{existing.cidr} instead of #{network_range.cidr} (both #{country})" + return existing end - end - - # If ancestor has same country, use it (persist if virtual) - if candidate_country == country - if !candidate.persisted? - candidate.save! - Rails.logger.info "Created ancestor network range #{candidate.cidr} for country #{country}" + else + # Create the IPAPI network range if it doesn't exist + begin + ipapi_network = NetworkRange.create!( + network: ipapi_cidr, + source: 'inherited', + country: country + ) + Rails.logger.info "Created IPAPI network range #{ipapi_cidr} for country #{country}" + return ipapi_network + rescue ActiveRecord::RecordNotUnique + # Race condition - another process created it + existing = NetworkRange.find_by(network: ipapi_cidr) + return existing || network_range end - Rails.logger.debug "Expanded #{network_range.cidr} to #{candidate.cidr} (both #{country})" - return candidate end end + # Fallback: Look for existing parent networks with IPAPI data and same country + # Query for all networks that contain this network and have IPAPI data + parent_with_ipapi = NetworkRange.where( + "?::inet << network", network_range.cidr + ).where( + "network_data ? 'ipapi' AND " \ + "network_data -> 'ipapi' ->> 'location' ->> 'country_code' = ?", + country + ).order("masklen(network) DESC").first + + if parent_with_ipapi + Rails.logger.debug "Found existing IPAPI parent #{parent_with_ipapi.cidr} for #{network_range.cidr} (both #{country})" + return parent_with_ipapi + end + + # No expansion possible - use original network network_range end