mirror of
https://github.com/zeek/zeek.git
synced 2025-10-02 06:38:20 +00:00

While we support initializing records via coercion from an expression list, e.g., local x: X = [$x1=1, $x2=2]; this can sometimes obscure the code to readers, e.g., when assigning to value declared and typed elsewhere. The language runtime has a similar overhead since instead of just constructing a known type it needs to check at runtime that the coercion from the expression list is valid; this can be slower than just writing the readible code in the first place, see #4559. With this patch we use explicit construction, e.g., local x = X($x1=1, $x2=2);
553 lines
18 KiB
Text
553 lines
18 KiB
Text
##! Implementation of catch-and-release functionality for NetControl.
|
|
|
|
@load base/frameworks/netcontrol
|
|
@load base/frameworks/cluster
|
|
|
|
module NetControl;
|
|
|
|
export {
|
|
|
|
redef enum Log::ID += { CATCH_RELEASE };
|
|
|
|
global log_policy_catch_release: Log::PolicyHook;
|
|
|
|
## This record is used for storing information about current blocks that are
|
|
## part of catch and release.
|
|
type BlockInfo: record {
|
|
## Absolute time indicating until when a block is inserted using NetControl.
|
|
block_until: time &optional;
|
|
## Absolute time indicating until when an IP address is watched to reblock it.
|
|
watch_until: time;
|
|
## Number of times an IP address was reblocked.
|
|
num_reblocked: count &default=0;
|
|
## Number indicating at which catch and release interval we currently are.
|
|
current_interval: count;
|
|
## ID of the inserted block, if any.
|
|
current_block_id: string;
|
|
## User specified string.
|
|
location: string &optional;
|
|
};
|
|
|
|
## The enum that contains the different kinds of messages that are logged by
|
|
## catch and release.
|
|
type CatchReleaseActions: enum {
|
|
## Log lines marked with info are purely informational; no action was taken.
|
|
INFO,
|
|
## A rule for the specified IP address already existed in NetControl (outside
|
|
## of catch-and-release). Catch and release did not add a new rule, but is now
|
|
## watching the IP address and will add a new rule after the current rule expires.
|
|
ADDED,
|
|
## A drop was requested by catch and release.
|
|
DROP_REQUESTED,
|
|
## An address was successfully blocked by catch and release.
|
|
DROPPED,
|
|
## An address was unblocked after the timeout expired.
|
|
UNBLOCK,
|
|
## An address was forgotten because it did not reappear within the `watch_until` interval.
|
|
FORGOTTEN,
|
|
## A watched IP address was seen again; catch and release will re-block it.
|
|
SEEN_AGAIN
|
|
};
|
|
|
|
## The record type that is used for representing and logging
|
|
type CatchReleaseInfo: record {
|
|
## The absolute time indicating when the action for this log-line occurred.
|
|
ts: time &log;
|
|
## The rule id that this log line refers to.
|
|
rule_id: string &log &optional;
|
|
## The IP address that this line refers to.
|
|
ip: addr &log;
|
|
## The action that was taken in this log-line.
|
|
action: CatchReleaseActions &log;
|
|
## The current block_interval (for how long the address is blocked).
|
|
block_interval: interval &log &optional;
|
|
## The current watch_interval (for how long the address will be watched and re-block if it reappears).
|
|
watch_interval: interval &log &optional;
|
|
## The absolute time until which the address is blocked.
|
|
blocked_until: time &log &optional;
|
|
## The absolute time until which the address will be monitored.
|
|
watched_until: time &log &optional;
|
|
## Number of times that this address was blocked in the current cycle.
|
|
num_blocked: count &log &optional;
|
|
## The user specified location string.
|
|
location: string &log &optional;
|
|
## Additional informational string by the catch and release framework about this log-line.
|
|
message: string &log &optional;
|
|
## Plugin triggering the log entry.
|
|
plugin: string &log &optional;
|
|
};
|
|
|
|
## Stops all packets involving an IP address from being forwarded. This function
|
|
## uses catch-and-release functionality, where the IP address is only dropped for
|
|
## a short amount of time that is incremented steadily when the IP is encountered
|
|
## again.
|
|
##
|
|
## In cluster mode, this function works on workers as well as the manager. On managers,
|
|
## the returned :zeek:see:`NetControl::BlockInfo` record will not contain the block ID,
|
|
## which will be assigned on the manager.
|
|
##
|
|
## a: The address to be dropped.
|
|
##
|
|
## t: How long to drop it, with 0 being indefinitely.
|
|
##
|
|
## location: An optional string describing where the drop was triggered.
|
|
##
|
|
## Returns: The :zeek:see:`NetControl::BlockInfo` record containing information about
|
|
## the inserted block.
|
|
global drop_address_catch_release: function(a: addr, location: string &default="") : BlockInfo;
|
|
|
|
## Removes an address from being watched with catch and release. Returns true if the
|
|
## address was found and removed; returns false if it was unknown to catch and release.
|
|
##
|
|
## If the address is currently blocked, and the block was inserted by catch and release,
|
|
## the block is removed.
|
|
##
|
|
## a: The address to be unblocked.
|
|
##
|
|
## reason: A reason for the unblock.
|
|
##
|
|
## Returns: True if the address was unblocked.
|
|
global unblock_address_catch_release: function(a: addr, reason: string &default="") : bool;
|
|
|
|
## This function can be called to notify the catch and release script that activity by
|
|
## an IP address was seen. If the respective IP address is currently monitored by catch and
|
|
## release and not blocked, the block will be reinstated. See the documentation of watch_new_connection
|
|
## which events the catch and release functionality usually monitors for activity.
|
|
##
|
|
## a: The address that was seen and should be re-dropped if it is being watched.
|
|
global catch_release_seen: function(a: addr);
|
|
|
|
## Get the :zeek:see:`NetControl::BlockInfo` record for an address currently blocked by catch and release.
|
|
## If the address is unknown to catch and release, the watch_until time will be set to 0.
|
|
##
|
|
## In cluster mode, this function works on the manager and workers. On workers, the data will
|
|
## lag slightly behind the manager; if you add a block, it will not be instantly available via
|
|
## this function.
|
|
##
|
|
## a: The address to get information about.
|
|
##
|
|
## Returns: The :zeek:see:`NetControl::BlockInfo` record containing information about
|
|
## the inserted block.
|
|
global get_catch_release_info: function(a: addr) : BlockInfo;
|
|
|
|
## Event is raised when catch and release cases management of an IP address because no
|
|
## activity was seen within the watch_until period.
|
|
##
|
|
## a: The address that is no longer being managed.
|
|
##
|
|
## bi: The :zeek:see:`NetControl::BlockInfo` record containing information about the block.
|
|
global catch_release_forgotten: event(a: addr, bi: BlockInfo);
|
|
|
|
## If true, catch_release_seen is called on the connection originator in new_connection,
|
|
## connection_established, partial_connection, connection_attempt, connection_rejected,
|
|
## connection_reset and connection_pending
|
|
const watch_connections = T &redef;
|
|
|
|
## If true, catch and release warns if packets of an IP address are still seen after it
|
|
## should have been blocked.
|
|
option catch_release_warn_blocked_ip_encountered = F;
|
|
|
|
## Time intervals for which subsequent drops of the same IP take
|
|
## effect.
|
|
const catch_release_intervals: vector of interval = vector(10min, 1hr, 24hrs, 7days) &redef;
|
|
|
|
## Event that can be handled to access the :zeek:type:`NetControl::CatchReleaseInfo`
|
|
## record as it is sent on to the logging framework.
|
|
global log_netcontrol_catch_release: event(rec: CatchReleaseInfo);
|
|
|
|
# Cluster events for catch and release
|
|
global catch_release_block_new: event(a: addr, b: BlockInfo);
|
|
global catch_release_block_delete: event(a: addr);
|
|
global catch_release_add: event(a: addr, location: string);
|
|
global catch_release_delete: event(a: addr, reason: string);
|
|
global catch_release_encountered: event(a: addr);
|
|
}
|
|
|
|
# Set that is used to only send seen notifications to the master every ~30 seconds.
|
|
global catch_release_recently_notified: set[addr] &create_expire=30secs;
|
|
|
|
event zeek_init() &priority=5
|
|
{
|
|
Log::create_stream(NetControl::CATCH_RELEASE, Log::Stream($columns=CatchReleaseInfo, $ev=log_netcontrol_catch_release, $path="netcontrol_catch_release", $policy=log_policy_catch_release));
|
|
}
|
|
|
|
function get_watch_interval(current_interval: count): interval
|
|
{
|
|
if ( (current_interval + 1) in catch_release_intervals )
|
|
return catch_release_intervals[current_interval+1];
|
|
else
|
|
return catch_release_intervals[current_interval];
|
|
}
|
|
|
|
function populate_log_record(ip: addr, bi: BlockInfo, action: CatchReleaseActions): CatchReleaseInfo
|
|
{
|
|
local log = CatchReleaseInfo($ts=network_time(), $ip=ip, $action=action,
|
|
$block_interval=catch_release_intervals[bi$current_interval],
|
|
$watch_interval=get_watch_interval(bi$current_interval),
|
|
$watched_until=bi$watch_until,
|
|
$num_blocked=bi$num_reblocked+1
|
|
);
|
|
|
|
if ( bi?$block_until )
|
|
log$blocked_until = bi$block_until;
|
|
|
|
if ( bi?$current_block_id && bi$current_block_id != "" )
|
|
log$rule_id = bi$current_block_id;
|
|
|
|
if ( bi?$location )
|
|
log$location = bi$location;
|
|
|
|
return log;
|
|
}
|
|
|
|
function per_block_interval(t: table[addr] of BlockInfo, idx: addr): interval
|
|
{
|
|
local remaining_time = t[idx]$watch_until - network_time();
|
|
if ( remaining_time < 0secs )
|
|
remaining_time = 0secs;
|
|
|
|
@if ( ! Cluster::is_enabled() || ( Cluster::is_enabled() && Cluster::local_node_type() == Cluster::MANAGER ) )
|
|
if ( remaining_time == 0secs )
|
|
{
|
|
local log = populate_log_record(idx, t[idx], FORGOTTEN);
|
|
Log::write(CATCH_RELEASE, log);
|
|
|
|
event NetControl::catch_release_forgotten(idx, t[idx]);
|
|
}
|
|
@endif
|
|
|
|
return remaining_time;
|
|
}
|
|
|
|
# This is the internally maintained table containing all the addresses that are currently being
|
|
# watched to see if they will re-surface. After the time is reached, monitoring of that specific
|
|
# IP will stop.
|
|
global blocks: table[addr] of BlockInfo = {}
|
|
&create_expire=0secs
|
|
&expire_func=per_block_interval;
|
|
|
|
function cr_check_rule(r: Rule): bool &is_used
|
|
{
|
|
if ( r$ty == DROP && r$entity$ty == ADDRESS )
|
|
{
|
|
local ip = r$entity$ip;
|
|
if ( ( is_v4_subnet(ip) && subnet_width(ip) == 32 ) || ( is_v6_subnet(ip) && subnet_width(ip) == 128 ) )
|
|
{
|
|
if ( subnet_to_addr(ip) in blocks )
|
|
return T;
|
|
}
|
|
}
|
|
|
|
return F;
|
|
}
|
|
|
|
@if ( ! Cluster::is_enabled() || ( Cluster::is_enabled() && Cluster::local_node_type() == Cluster::MANAGER ) )
|
|
|
|
event rule_added(r: Rule, p: PluginState, msg: string)
|
|
{
|
|
if ( !cr_check_rule(r) )
|
|
return;
|
|
|
|
local ip = subnet_to_addr(r$entity$ip);
|
|
local bi = blocks[ip];
|
|
|
|
local log = populate_log_record(ip, bi, DROPPED);
|
|
log$plugin = p$plugin$name(p);
|
|
if ( msg != "" )
|
|
log$message = msg;
|
|
Log::write(CATCH_RELEASE, log);
|
|
}
|
|
|
|
event rule_exists(r: Rule, p: PluginState, msg: string)
|
|
{
|
|
if ( !cr_check_rule(r) )
|
|
return;
|
|
|
|
local ip = subnet_to_addr(r$entity$ip);
|
|
local bi = blocks[ip];
|
|
|
|
local log = populate_log_record(ip, bi, INFO);
|
|
log$plugin = p$plugin$name(p);
|
|
local infomsg = "Existing rule encountered while inserting rule";
|
|
log$message = msg + infomsg;
|
|
Log::write(CATCH_RELEASE, log);
|
|
}
|
|
|
|
event rule_error(r: Rule, p: PluginState, msg: string)
|
|
{
|
|
if ( !cr_check_rule(r) )
|
|
return;
|
|
|
|
local ip = subnet_to_addr(r$entity$ip);
|
|
local bi = blocks[ip];
|
|
|
|
local log = populate_log_record(ip, bi, INFO);
|
|
log$plugin = p$plugin$name(p);
|
|
log$message = "Error occurred during rule operation: " + msg;
|
|
Log::write(CATCH_RELEASE, log);
|
|
}
|
|
|
|
event rule_timeout(r: Rule, i: FlowInfo, p: PluginState)
|
|
{
|
|
if ( !cr_check_rule(r) )
|
|
return;
|
|
|
|
local ip = subnet_to_addr(r$entity$ip);
|
|
local bi = blocks[ip];
|
|
|
|
local log = populate_log_record(ip, bi, UNBLOCK);
|
|
log$plugin = p$plugin$name(p);
|
|
if ( bi?$block_until )
|
|
{
|
|
local difference: interval = network_time() - bi$block_until;
|
|
if ( interval_to_double(difference) > 60 || interval_to_double(difference) < -60 )
|
|
log$message = fmt("Difference between network_time and block time excessive: %f", difference);
|
|
}
|
|
|
|
Log::write(CATCH_RELEASE, log);
|
|
}
|
|
|
|
@endif
|
|
|
|
@if ( Cluster::is_enabled() && Cluster::local_node_type() == Cluster::MANAGER )
|
|
event catch_release_add(a: addr, location: string)
|
|
{
|
|
drop_address_catch_release(a, location);
|
|
}
|
|
|
|
event catch_release_delete(a: addr, reason: string)
|
|
{
|
|
unblock_address_catch_release(a, reason);
|
|
}
|
|
|
|
event catch_release_encountered(a: addr)
|
|
{
|
|
catch_release_seen(a);
|
|
}
|
|
@endif
|
|
|
|
@if ( Cluster::is_enabled() && Cluster::local_node_type() != Cluster::MANAGER )
|
|
event catch_release_block_new(a: addr, b: BlockInfo)
|
|
{
|
|
blocks[a] = b;
|
|
}
|
|
|
|
event catch_release_block_delete(a: addr)
|
|
{
|
|
if ( a in blocks )
|
|
delete blocks[a];
|
|
}
|
|
@endif
|
|
|
|
@if ( Cluster::is_enabled() && Cluster::local_node_type() == Cluster::MANAGER )
|
|
@endif
|
|
|
|
function get_catch_release_info(a: addr): BlockInfo
|
|
{
|
|
if ( a in blocks )
|
|
return blocks[a];
|
|
|
|
return BlockInfo($watch_until=double_to_time(0), $current_interval=0, $current_block_id="");
|
|
}
|
|
|
|
function drop_address_catch_release(a: addr, location: string &default=""): BlockInfo
|
|
{
|
|
local bi: BlockInfo;
|
|
local log: CatchReleaseInfo;
|
|
|
|
if ( a in blocks )
|
|
{
|
|
log = populate_log_record(a, blocks[a], INFO);
|
|
log$message = "Already blocked using catch-and-release - ignoring duplicate";
|
|
Log::write(CATCH_RELEASE, log);
|
|
|
|
return blocks[a];
|
|
}
|
|
|
|
local e = Entity($ty=ADDRESS, $ip=addr_to_subnet(a));
|
|
if ( [e,DROP] in rule_entities )
|
|
{
|
|
local r = rule_entities[e,DROP];
|
|
|
|
bi = BlockInfo($watch_until=network_time()+catch_release_intervals[1], $current_interval=0, $current_block_id=r$id);
|
|
if ( location != "" )
|
|
bi$location = location;
|
|
@if ( ! Cluster::is_enabled() || ( Cluster::is_enabled() && Cluster::local_node_type() == Cluster::MANAGER ) )
|
|
log = populate_log_record(a, bi, ADDED);
|
|
log$message = "Address already blocked outside of catch-and-release. Catch and release will monitor and only actively block if it appears in network traffic.";
|
|
Log::write(CATCH_RELEASE, log);
|
|
blocks[a] = bi;
|
|
@if ( Cluster::is_enabled() )
|
|
Cluster::publish(Cluster::worker_topic, NetControl::catch_release_block_new, a, bi);
|
|
@endif
|
|
@endif
|
|
|
|
@if ( Cluster::is_enabled() && Cluster::local_node_type() != Cluster::MANAGER )
|
|
Cluster::publish(Cluster::manager_topic, NetControl::catch_release_add, a, location);
|
|
@endif
|
|
return bi;
|
|
}
|
|
|
|
# No entry in blocks.
|
|
local block_interval = catch_release_intervals[0];
|
|
|
|
@if ( ! Cluster::is_enabled() || ( Cluster::is_enabled() && Cluster::local_node_type() == Cluster::MANAGER ) )
|
|
local ret = drop_address(a, block_interval, location);
|
|
|
|
if ( ret != "" )
|
|
{
|
|
bi = BlockInfo($watch_until=network_time()+catch_release_intervals[1], $block_until=network_time()+block_interval, $current_interval=0, $current_block_id=ret);
|
|
if ( location != "" )
|
|
bi$location = location;
|
|
blocks[a] = bi;
|
|
@if ( Cluster::is_enabled() )
|
|
Cluster::publish(Cluster::worker_topic, NetControl::catch_release_block_new, a, bi);
|
|
@endif
|
|
log = populate_log_record(a, bi, DROP_REQUESTED);
|
|
Log::write(CATCH_RELEASE, log);
|
|
return bi;
|
|
}
|
|
Reporter::error(fmt("Catch and release could not add block for %s; failing.", a));
|
|
return BlockInfo($watch_until=double_to_time(0), $current_interval=0, $current_block_id="");
|
|
@endif
|
|
|
|
@if ( Cluster::is_enabled() && Cluster::local_node_type() != Cluster::MANAGER )
|
|
bi = BlockInfo($watch_until=network_time()+catch_release_intervals[1], $block_until=network_time()+block_interval, $current_interval=0, $current_block_id="");
|
|
Cluster::publish(Cluster::manager_topic, NetControl::catch_release_add, a, location);
|
|
return bi;
|
|
@endif
|
|
|
|
}
|
|
|
|
function unblock_address_catch_release(a: addr, reason: string &default=""): bool
|
|
{
|
|
if ( a !in blocks )
|
|
return F;
|
|
|
|
@if ( ! Cluster::is_enabled() || ( Cluster::is_enabled() && Cluster::local_node_type() == Cluster::MANAGER ) )
|
|
local bi = blocks[a];
|
|
local log = populate_log_record(a, bi, UNBLOCK);
|
|
if ( reason != "" )
|
|
log$message = reason;
|
|
Log::write(CATCH_RELEASE, log);
|
|
delete blocks[a];
|
|
if ( bi?$block_until && bi$block_until > network_time() && bi$current_block_id != "" )
|
|
remove_rule(bi$current_block_id, reason);
|
|
@endif
|
|
@if ( Cluster::is_enabled() && Cluster::local_node_type() == Cluster::MANAGER )
|
|
Cluster::publish(Cluster::worker_topic, NetControl::catch_release_block_delete, a);
|
|
@endif
|
|
@if ( Cluster::is_enabled() && Cluster::local_node_type() != Cluster::MANAGER )
|
|
Cluster::publish(Cluster::manager_topic, NetControl::catch_release_delete, a, reason);
|
|
@endif
|
|
|
|
return T;
|
|
}
|
|
|
|
function catch_release_seen(a: addr)
|
|
{
|
|
if ( a in blocks )
|
|
{
|
|
@if ( ! Cluster::is_enabled() || ( Cluster::is_enabled() && Cluster::local_node_type() == Cluster::MANAGER ) )
|
|
local bi = blocks[a];
|
|
local log: CatchReleaseInfo;
|
|
local e = Entity($ty=ADDRESS, $ip=addr_to_subnet(a));
|
|
|
|
if ( [e,DROP] in rule_entities )
|
|
{
|
|
if ( catch_release_warn_blocked_ip_encountered == F )
|
|
return;
|
|
|
|
# This should be blocked - block has not been applied yet by hardware? Ignore for the moment...
|
|
log = populate_log_record(a, bi, INFO);
|
|
log$action = INFO;
|
|
log$message = "Block seen while in rule_entities. No action taken.";
|
|
Log::write(CATCH_RELEASE, log);
|
|
return;
|
|
}
|
|
|
|
# ok, this one returned again while still in the backoff period.
|
|
|
|
local try = bi$current_interval;
|
|
if ( (try+1) in catch_release_intervals )
|
|
++try;
|
|
|
|
bi$current_interval = try;
|
|
if ( (try+1) in catch_release_intervals )
|
|
bi$watch_until = network_time() + catch_release_intervals[try+1];
|
|
else
|
|
bi$watch_until = network_time() + catch_release_intervals[try];
|
|
|
|
bi$block_until = network_time() + catch_release_intervals[try];
|
|
++bi$num_reblocked;
|
|
|
|
local block_interval = catch_release_intervals[try];
|
|
local location = "";
|
|
if ( bi?$location )
|
|
location = bi$location;
|
|
local drop = drop_address(a, block_interval, fmt("Re-drop by catch-and-release: %s", location));
|
|
bi$current_block_id = drop;
|
|
|
|
blocks[a] = bi;
|
|
|
|
log = populate_log_record(a, bi, SEEN_AGAIN);
|
|
Log::write(CATCH_RELEASE, log);
|
|
@endif
|
|
@if ( Cluster::is_enabled() && Cluster::local_node_type() == Cluster::MANAGER )
|
|
Cluster::publish(Cluster::worker_topic, NetControl::catch_release_block_new, a, bi);
|
|
@endif
|
|
@if ( Cluster::is_enabled() && Cluster::local_node_type() != Cluster::MANAGER )
|
|
if ( a in catch_release_recently_notified )
|
|
return;
|
|
|
|
Cluster::publish(Cluster::manager_topic, NetControl::catch_release_encountered, a);
|
|
add catch_release_recently_notified[a];
|
|
@endif
|
|
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
event new_connection(c: connection)
|
|
{
|
|
if ( watch_connections )
|
|
catch_release_seen(c$id$orig_h);
|
|
}
|
|
|
|
event connection_established(c: connection)
|
|
{
|
|
if ( watch_connections )
|
|
catch_release_seen(c$id$orig_h);
|
|
}
|
|
|
|
event partial_connection(c: connection)
|
|
{
|
|
if ( watch_connections )
|
|
catch_release_seen(c$id$orig_h);
|
|
}
|
|
|
|
event connection_attempt(c: connection)
|
|
{
|
|
if ( watch_connections )
|
|
catch_release_seen(c$id$orig_h);
|
|
}
|
|
|
|
event connection_rejected(c: connection)
|
|
{
|
|
if ( watch_connections )
|
|
catch_release_seen(c$id$orig_h);
|
|
}
|
|
|
|
event connection_reset(c: connection)
|
|
{
|
|
if ( watch_connections )
|
|
catch_release_seen(c$id$orig_h);
|
|
}
|
|
|
|
event connection_pending(c: connection)
|
|
{
|
|
if ( watch_connections )
|
|
catch_release_seen(c$id$orig_h);
|
|
}
|