punish by node id

This commit is contained in:
Christien Rioux
2023-07-15 19:32:53 -04:00
parent 80cb23c0c6
commit 3264b568d0
15 changed files with 202 additions and 64 deletions

View File

@@ -3,6 +3,7 @@ use alloc::collections::btree_map::Entry;
// XXX: Move to config eventually?
const PUNISHMENT_DURATION_MIN: usize = 60;
const MAX_PUNISHMENTS_BY_NODE_ID: usize = 65536;
#[derive(ThisError, Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddressFilterError {
@@ -26,15 +27,37 @@ struct AddressFilterInner {
conn_timestamps_by_ip6_prefix: BTreeMap<Ipv6Addr, Vec<Timestamp>>,
punishments_by_ip4: BTreeMap<Ipv4Addr, Timestamp>,
punishments_by_ip6_prefix: BTreeMap<Ipv6Addr, Timestamp>,
punishments_by_node_id: BTreeMap<TypedKey, Timestamp>,
}
#[derive(Debug)]
struct AddressFilterUnlockedInner {
max_connections_per_ip4: usize,
max_connections_per_ip6_prefix: usize,
max_connections_per_ip6_prefix_size: usize,
max_connection_frequency_per_min: usize,
punishment_duration_min: usize,
routing_table: RoutingTable,
}
impl fmt::Debug for AddressFilterUnlockedInner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AddressFilterUnlockedInner")
.field("max_connections_per_ip4", &self.max_connections_per_ip4)
.field(
"max_connections_per_ip6_prefix",
&self.max_connections_per_ip6_prefix,
)
.field(
"max_connections_per_ip6_prefix_size",
&self.max_connections_per_ip6_prefix_size,
)
.field(
"max_connection_frequency_per_min",
&self.max_connection_frequency_per_min,
)
.field("punishment_duration_min", &self.punishment_duration_min)
.finish()
}
}
#[derive(Clone, Debug)]
@@ -44,7 +67,7 @@ pub struct AddressFilter {
}
impl AddressFilter {
pub fn new(config: VeilidConfig) -> Self {
pub fn new(config: VeilidConfig, routing_table: RoutingTable) -> Self {
let c = config.get();
Self {
unlocked_inner: Arc::new(AddressFilterUnlockedInner {
@@ -55,6 +78,7 @@ impl AddressFilter {
max_connection_frequency_per_min: c.network.max_connection_frequency_per_min
as usize,
punishment_duration_min: PUNISHMENT_DURATION_MIN,
routing_table,
}),
inner: Arc::new(Mutex::new(AddressFilterInner {
conn_count_by_ip4: BTreeMap::new(),
@@ -63,6 +87,7 @@ impl AddressFilter {
conn_timestamps_by_ip6_prefix: BTreeMap::new(),
punishments_by_ip4: BTreeMap::new(),
punishments_by_ip6_prefix: BTreeMap::new(),
punishments_by_node_id: BTreeMap::new(),
})),
}
}
@@ -135,9 +160,29 @@ impl AddressFilter {
inner.punishments_by_ip6_prefix.remove(&key);
}
}
// node id
{
let mut dead_keys = Vec::<TypedKey>::new();
for (key, value) in &mut inner.punishments_by_node_id {
// Drop punishments older than the punishment duration
if cur_ts.as_u64().saturating_sub(value.as_u64())
> self.unlocked_inner.punishment_duration_min as u64 * 60_000_000u64
{
dead_keys.push(*key);
}
}
for key in dead_keys {
log_net!(debug ">>> FORGIVING: {}", key);
inner.punishments_by_node_id.remove(&key);
// make the entry alive again if it's still here
if let Ok(Some(nr)) = self.unlocked_inner.routing_table.lookup_node_ref(key) {
nr.operate_mut(|_rti, e| e.set_punished(false));
}
}
}
}
fn is_punished_inner(&self, inner: &AddressFilterInner, ipblock: IpAddr) -> bool {
fn is_ip_addr_punished_inner(&self, inner: &AddressFilterInner, ipblock: IpAddr) -> bool {
match ipblock {
IpAddr::V4(v4) => {
if inner.punishments_by_ip4.contains_key(&v4) {
@@ -153,16 +198,16 @@ impl AddressFilter {
false
}
pub fn is_punished(&self, addr: IpAddr) -> bool {
pub fn is_ip_addr_punished(&self, addr: IpAddr) -> bool {
let inner = self.inner.lock();
let ipblock = ip_to_ipblock(
self.unlocked_inner.max_connections_per_ip6_prefix_size,
addr,
);
self.is_punished_inner(&*inner, ipblock)
self.is_ip_addr_punished_inner(&*inner, ipblock)
}
pub fn punish(&self, addr: IpAddr) {
pub fn punish_ip_addr(&self, addr: IpAddr) {
log_net!(debug ">>> PUNISHED: {}", addr);
let ts = get_aligned_timestamp();
@@ -186,6 +231,39 @@ impl AddressFilter {
};
}
fn is_node_id_punished_inner(&self, inner: &AddressFilterInner, node_id: TypedKey) -> bool {
if inner.punishments_by_node_id.contains_key(&node_id) {
return true;
}
false
}
pub fn is_node_id_punished(&self, node_id: TypedKey) -> bool {
let inner = self.inner.lock();
self.is_node_id_punished_inner(&*inner, node_id)
}
pub fn punish_node_id(&self, node_id: TypedKey) {
if let Ok(Some(nr)) = self.unlocked_inner.routing_table.lookup_node_ref(node_id) {
// make the entry dead if it's punished
nr.operate_mut(|_rti, e| e.set_punished(true));
}
let ts = get_aligned_timestamp();
let mut inner = self.inner.lock();
if inner.punishments_by_node_id.len() >= MAX_PUNISHMENTS_BY_NODE_ID {
log_net!(debug ">>> PUNISHMENT TABLE FULL: {}", node_id);
return;
}
log_net!(debug ">>> PUNISHED: {}", node_id);
inner
.punishments_by_node_id
.entry(node_id)
.and_modify(|v| *v = ts)
.or_insert(ts);
}
pub async fn address_filter_task_routine(
self,
_stop_token: StopToken,
@@ -207,7 +285,7 @@ impl AddressFilter {
self.unlocked_inner.max_connections_per_ip6_prefix_size,
addr,
);
if self.is_punished_inner(inner, ipblock) {
if self.is_ip_addr_punished_inner(inner, ipblock) {
return Err(AddressFilterError::Punished);
}

View File

@@ -143,9 +143,9 @@ struct NetworkManagerUnlockedInner {
#[cfg(feature = "unstable-blockstore")]
block_store: BlockStore,
crypto: Crypto,
address_filter: AddressFilter,
// Accessors
routing_table: RwLock<Option<RoutingTable>>,
address_filter: RwLock<Option<AddressFilter>>,
components: RwLock<Option<NetworkComponents>>,
update_callback: RwLock<Option<UpdateCallback>>,
// Background processes
@@ -189,7 +189,7 @@ impl NetworkManager {
#[cfg(feature = "unstable-blockstore")]
block_store,
crypto,
address_filter: AddressFilter::new(config),
address_filter: RwLock::new(None),
routing_table: RwLock::new(None),
components: RwLock::new(None),
update_callback: RwLock::new(None),
@@ -292,7 +292,12 @@ impl NetworkManager {
self.unlocked_inner.crypto.clone()
}
pub fn address_filter(&self) -> AddressFilter {
self.unlocked_inner.address_filter.clone()
self.unlocked_inner
.address_filter
.read()
.as_ref()
.unwrap()
.clone()
}
pub fn routing_table(&self) -> RoutingTable {
self.unlocked_inner
@@ -351,7 +356,9 @@ impl NetworkManager {
pub async fn init(&self, update_callback: UpdateCallback) -> EyreResult<()> {
let routing_table = RoutingTable::new(self.clone());
routing_table.init().await?;
let address_filter = AddressFilter::new(self.config(), routing_table.clone());
*self.unlocked_inner.routing_table.write() = Some(routing_table.clone());
*self.unlocked_inner.address_filter.write() = Some(address_filter);
*self.unlocked_inner.update_callback.write() = Some(update_callback);
Ok(())
}
@@ -904,7 +911,7 @@ impl NetworkManager {
// Ensure we can read the magic number
if data.len() < 4 {
log_net!(debug "short packet");
self.address_filter().punish(remote_addr);
self.address_filter().punish_ip_addr(remote_addr);
return Ok(false);
}
@@ -939,7 +946,7 @@ impl NetworkManager {
Ok(v) => v,
Err(e) => {
log_net!(debug "envelope failed to decode: {}", e);
self.address_filter().punish(remote_addr);
self.address_filter().punish_ip_addr(remote_addr);
return Ok(false);
}
};
@@ -991,6 +998,10 @@ impl NetworkManager {
// Peek at header and see if we need to relay this
// If the recipient id is not our node id, then it needs relaying
let sender_id = TypedKey::new(envelope.get_crypto_kind(), envelope.get_sender_id());
if self.address_filter().is_node_id_punished(sender_id) {
return Ok(false);
}
let recipient_id = TypedKey::new(envelope.get_crypto_kind(), envelope.get_recipient_id());
if !routing_table.matches_own_node_id(&[recipient_id]) {
// See if the source node is allowed to resolve nodes
@@ -1070,7 +1081,7 @@ impl NetworkManager {
Ok(v) => v,
Err(e) => {
log_net!(debug "failed to decrypt envelope body: {}",e);
self.address_filter().punish(remote_addr);
self.address_filter().punish_ip_addr(remote_addr);
return Ok(false);
}
};

View File

@@ -410,7 +410,7 @@ impl Network {
if self
.network_manager()
.address_filter()
.is_punished(dial_info.address().to_ip_addr())
.is_ip_addr_punished(dial_info.address().to_ip_addr())
{
return Ok(NetworkResult::no_connection_other("punished"));
}
@@ -477,7 +477,7 @@ impl Network {
if self
.network_manager()
.address_filter()
.is_punished(dial_info.address().to_ip_addr())
.is_ip_addr_punished(dial_info.address().to_ip_addr())
{
return Ok(NetworkResult::no_connection_other("punished"));
}

View File

@@ -120,7 +120,7 @@ impl Network {
};
// Check to see if it is punished
let address_filter = self.network_manager().address_filter();
if address_filter.is_punished(peer_addr.ip()) {
if address_filter.is_ip_addr_punished(peer_addr.ip()) {
return;
}

View File

@@ -24,7 +24,7 @@ impl ProtocolNetworkConnection {
timeout_ms: u32,
address_filter: AddressFilter,
) -> io::Result<NetworkResult<ProtocolNetworkConnection>> {
if address_filter.is_punished(dial_info.address().to_ip_addr()) {
if address_filter.is_ip_addr_punished(dial_info.address().to_ip_addr()) {
return Ok(NetworkResult::no_connection_other("punished"));
}
match dial_info.protocol_type() {

View File

@@ -25,7 +25,7 @@ impl RawUdpProtocolHandler {
// Check to see if it is punished
if let Some(af) = self.address_filter.as_ref() {
if af.is_punished(remote_addr.ip()) {
if af.is_ip_addr_punished(remote_addr.ip()) {
continue;
}
}
@@ -97,7 +97,7 @@ impl RawUdpProtocolHandler {
// Check to see if it is punished
if let Some(af) = self.address_filter.as_ref() {
if af.is_punished(remote_addr.ip()) {
if af.is_ip_addr_punished(remote_addr.ip()) {
return Ok(NetworkResult::no_connection_other("punished"));
}
}

View File

@@ -305,7 +305,7 @@ impl NetworkConnection {
let peer_address = protocol_connection.descriptor().remote();
// Check to see if it is punished
if address_filter.is_punished(peer_address.to_socket_addr().ip()) {
if address_filter.is_ip_addr_punished(peer_address.to_socket_addr().ip()) {
return RecvLoopAction::Finish;
}

View File

@@ -307,6 +307,11 @@ impl NetworkManager {
) -> EyreResult<NodeContactMethod> {
let routing_table = self.routing_table();
// If a node is punished, then don't try to contact it
if target_node_ref.node_ids().iter().find(|nid| self.address_filter().is_node_id_punished(**nid)).is_some() {
return Ok(NodeContactMethod::Unreachable);
}
// Figure out the best routing domain to get the contact method over
let routing_domain = match target_node_ref.best_routing_domain() {
Some(rd) => rd,

View File

@@ -2,10 +2,11 @@ use super::*;
use super::connection_table::*;
use crate::tests::common::test_veilid_config::*;
use crate::tests::mock_routing_table;
pub async fn test_add_get_remove() {
let config = get_config();
let address_filter = AddressFilter::new(config.clone());
let address_filter = AddressFilter::new(config.clone(), mock_routing_table());
let table = ConnectionTable::new(config, address_filter);
let a1 = ConnectionDescriptor::new_no_local(PeerAddress::new(

View File

@@ -133,7 +133,7 @@ impl Network {
if self
.network_manager()
.address_filter()
.is_punished(dial_info.address().to_ip_addr())
.is_ip_addr_punished(dial_info.address().to_ip_addr())
{
return Ok(NetworkResult::no_connection_other("punished"));
}
@@ -182,7 +182,7 @@ impl Network {
if self
.network_manager()
.address_filter()
.is_punished(dial_info.address().to_ip_addr())
.is_ip_addr_punished(dial_info.address().to_ip_addr())
{
return Ok(NetworkResult::no_connection_other("punished"));
}

View File

@@ -19,7 +19,7 @@ impl ProtocolNetworkConnection {
timeout_ms: u32,
address_filter: AddressFilter,
) -> io::Result<NetworkResult<ProtocolNetworkConnection>> {
if address_filter.is_punished(dial_info.address().to_ip_addr()) {
if address_filter.is_ip_addr_punished(dial_info.address().to_ip_addr()) {
return Ok(NetworkResult::no_connection_other("punished"));
}
match dial_info.protocol_type() {