discourse/app/models/screened_ip_address.rb
Bianca Nenciu 86c7e07428
FEATURE: Improve screened IPs roll up and extend for IPv6 ()
This commit improves the logic for rolling up IPv4 screened IP
addresses and extending it for IPv6. IPv4 addresses will roll up only
up to /24. IPv6 can rollup to /48 at most. The log message that is
generated contains the list of original IPs and new subnet.
2022-04-12 21:07:37 +03:00

181 lines
5.5 KiB
Ruby

# frozen_string_literal: true
require 'screening_model'
# A ScreenedIpAddress record represents an IP address or subnet that is being watched,
# and possibly blocked from creating accounts.
class ScreenedIpAddress < ActiveRecord::Base
include ScreeningModel
default_action :block
validates :ip_address, ip_address_format: true, presence: true
after_validation :check_for_match
ROLLED_UP_BLOCKS = [
# IPv4
[4, 32, 24],
# IPv6
[6, (65..128).to_a, 64],
[6, 64, 60],
[6, 60, 56],
[6, 56, 52],
[6, 52, 48],
]
def self.watch(ip_address, opts = {})
match_for_ip_address(ip_address) || create(opts.slice(:action_type).merge(ip_address: ip_address))
end
def check_for_match
unless self.errors[:ip_address].present?
matched = self.class.match_for_ip_address(self.ip_address)
if matched && matched.action_type == self.action_type
self.errors.add(:ip_address, :ip_address_already_screened)
end
end
end
# In Rails 4.0.0, validators are run to handle invalid assignments to inet columns (as they should).
# In Rails 4.0.1, an exception is raised before validation happens, so we need this hack for
# inet/cidr columns:
def ip_address=(val)
if val.nil?
self.errors.add(:ip_address, :invalid)
return
end
if val.is_a?(IPAddr)
write_attribute(:ip_address, val)
return
end
v = IPAddr.handle_wildcards(val)
if v.nil?
self.errors.add(:ip_address, :invalid)
return
end
write_attribute(:ip_address, v)
# this gets even messier, Ruby 1.9.2 raised a different exception to Ruby 2.0.0
# handle both exceptions
rescue ArgumentError, IPAddr::InvalidAddressError
self.errors.add(:ip_address, :invalid)
end
# Return a string with the ip address and mask in standard format. e.g., "127.0.0.0/8".
def ip_address_with_mask
ip_address.try(:to_cidr_s)
end
def self.match_for_ip_address(ip_address)
# The <<= operator on inet columns means "is contained within or equal to".
#
# Read more about PostgreSQL's inet data type here:
#
# http://www.postgresql.org/docs/9.1/static/datatype-net-types.html
# http://www.postgresql.org/docs/9.1/static/functions-net.html
ip_address = IPAddr === ip_address ? ip_address.to_cidr_s : ip_address.to_s
order('masklen(ip_address) DESC').find_by("? <<= ip_address", ip_address)
end
def self.should_block?(ip_address)
exists_for_ip_address_and_action?(ip_address, actions[:block])
end
def self.is_allowed?(ip_address)
exists_for_ip_address_and_action?(ip_address, actions[:do_nothing])
end
def self.exists_for_ip_address_and_action?(ip_address, action_type, opts = {})
b = match_for_ip_address(ip_address)
found = !!b && b.action_type == action_type
b.record_match! if found && opts[:record_match] != false
found
end
def self.block_admin_login?(user, ip_address)
return false unless SiteSetting.use_admin_ip_allowlist
return false if user.nil?
return false if !user.admin?
return false if ScreenedIpAddress.where(action_type: actions[:allow_admin]).count == 0
return true if ip_address.nil?
!exists_for_ip_address_and_action?(ip_address, actions[:allow_admin], record_match: false)
end
def self.subnets(family, from_masklen, to_masklen)
sql = <<~SQL
WITH ips_and_subnets AS (
SELECT ip_address,
network(inet(host(ip_address) || '/' || :to_masklen))::text subnet
FROM screened_ip_addresses
WHERE family(ip_address) = :family AND
masklen(ip_address) IN (:from_masklen) AND
action_type = :blocked
)
SELECT subnet
FROM ips_and_subnets
GROUP BY subnet
HAVING COUNT(*) >= :min_ban_entries_for_roll_up
SQL
DB.query_single(
sql,
family: family,
from_masklen: from_masklen,
to_masklen: to_masklen,
blocked: ScreenedIpAddress.actions[:block],
min_ban_entries_for_roll_up: SiteSetting.min_ban_entries_for_roll_up,
)
end
def self.roll_up(current_user = Discourse.system_user)
ROLLED_UP_BLOCKS.each do |family, from_masklen, to_masklen|
ScreenedIpAddress.subnets(family, from_masklen, to_masklen).map do |subnet|
next if ScreenedIpAddress.where("? <<= ip_address", subnet).exists?
old_ips = ScreenedIpAddress
.where(action_type: ScreenedIpAddress.actions[:block])
.where("ip_address << ?", subnet)
.where("family(ip_address) = ?", family)
.where("masklen(ip_address) IN (?)", from_masklen)
sum_match_count, max_last_match_at, min_created_at =
old_ips.pluck_first('SUM(match_count), MAX(last_match_at), MIN(created_at)')
ScreenedIpAddress.create!(
ip_address: subnet,
match_count: sum_match_count,
last_match_at: max_last_match_at,
created_at: min_created_at,
)
StaffActionLogger.new(current_user).log_roll_up(subnet, old_ips.map(&:ip_address))
old_ips.delete_all
end
end
end
end
# == Schema Information
#
# Table name: screened_ip_addresses
#
# id :integer not null, primary key
# ip_address :inet not null
# action_type :integer not null
# match_count :integer default(0), not null
# last_match_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_screened_ip_addresses_on_ip_address (ip_address) UNIQUE
# index_screened_ip_addresses_on_last_match_at (last_match_at)
#