Find supernets, don't create them

This commit is contained in:
Dan Milne
2025-12-27 11:56:19 +11:00
parent 108caf2fe6
commit e53e782223
2 changed files with 41 additions and 56 deletions

View File

@@ -136,33 +136,6 @@ class NetworkRange < ApplicationRecord
.order("masklen(network) ASC") # Least specific child first .order("masklen(network) ASC") # Least specific child first
end 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 # Find nearest parent with intelligence data
def parent_with_intelligence def parent_with_intelligence
# Find all parent ranges (networks that contain this network) # Find all parent ranges (networks that contain this network)

View File

@@ -517,41 +517,53 @@ validate :targets_must_be_array
country = network_range.country || network_range.inherited_intelligence[:country] country = network_range.country || network_range.inherited_intelligence[:country]
return network_range unless country return network_range unless country
# Walk up from current prefix to /8 (IPv4) or /32 to /1 (IPv6) # Check if this network has IPAPI data with a larger CIDR (asn.route or ipapi_returned_cidr)
current_prefix = network_range.prefix_length ipapi_cidr = network_range.network_data&.dig('ipapi', 'asn', 'route') ||
max_prefix = network_range.ipv4? ? 8 : 1 network_range.network_data&.dig('ipapi_returned_cidr')
current_prefix.step(-1, -1).each do |prefix| if ipapi_cidr && ipapi_cidr != network_range.cidr
break if prefix < max_prefix # IPAPI returned a larger network - use it if it exists
existing = NetworkRange.find_by(network: ipapi_cidr)
candidate = network_range.find_or_create_ancestor_at_prefix(prefix) if existing
# Check if candidate has geo data (either direct or inherited) existing_country = existing.country || existing.inherited_intelligence[:country]
candidate_country = candidate.country || (candidate.persisted? ? candidate.inherited_intelligence[:country] : nil) if existing_country == country
Rails.logger.debug "Using IPAPI CIDR #{existing.cidr} instead of #{network_range.cidr} (both #{country})"
# For virtual (unpersisted) ancestors, we need to check if any existing records in that range have the country return existing
if candidate_country.nil? && !candidate.persisted? end
# Query database to see if networks in this range have the same country else
country_check = NetworkRange.where("network <<= ?", candidate.cidr) # Create the IPAPI network range if it doesn't exist
.where.not(country: nil) begin
.select(:country) ipapi_network = NetworkRange.create!(
.distinct network: ipapi_cidr,
# If all networks in this range have the same country, we can use it source: 'inherited',
if country_check.count == 1 && country_check.first.country == country country: country
candidate_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
end end
end end
# If ancestor has same country, use it (persist if virtual) # Fallback: Look for existing parent networks with IPAPI data and same country
if candidate_country == country # Query for all networks that contain this network and have IPAPI data
if !candidate.persisted? parent_with_ipapi = NetworkRange.where(
candidate.save! "?::inet << network", network_range.cidr
Rails.logger.info "Created ancestor network range #{candidate.cidr} for country #{country}" ).where(
end "network_data ? 'ipapi' AND " \
Rails.logger.debug "Expanded #{network_range.cidr} to #{candidate.cidr} (both #{country})" "network_data -> 'ipapi' ->> 'location' ->> 'country_code' = ?",
return candidate country
end ).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 end
# No expansion possible - use original network
network_range network_range
end end