11 KiB
Adversarial Review: Eclipse Attack на Montana
Модель: Claude Opus 4.5 Компания: Anthropic Дата: 07.01.2026 17:00 UTC Роль: Атакующий с неограниченными ресурсами
Attack Surface
External inputs:
- P2P addr messages (адреса пиров)
- DNS seeds (hardcoded в бинарнике)
- Gossip от подключённых пиров
- AddrMan persistence file (peers.dat)
Trust boundaries:
- Сеть → AddrMan (NEW table)
- Успешное соединение → TRIED table
- AddrMan file → Memory
Critical assets:
- Outbound connections (8 слотов)
- Inbound connections (117 слотов)
- Chain tip consensus
Attempted Attacks
1. NEW Table Poisoning
| # | Attack | Target | Result |
|---|---|---|---|
| 1.1 | Отправить 1M вредоносных адресов через addr message | NEW table | PROTECTED — MAX_ADDR_SIZE=1000 ограничивает одно сообщение |
| 1.2 | Отправить addr от множества connection | NEW table | PROTECTED — bucket assignment зависит от source IP |
| 1.3 | Заполнить все 1024 bucket одним netgroup | NEW table | PROTECTED — bucket = hash(key || netgroup || source_netgroup) |
Анализ addrman.rs:441-450:
fn get_new_bucket(&self, addr: &SocketAddr, source: Option<&SocketAddr>) -> usize {
let mut hasher = SipHasher24::new_with_key(&self.key[..16]);
hasher.write(&get_netgroup_bytes(addr));
if let Some(src) = source {
hasher.write(&get_netgroup_bytes(src)); // Source affects bucket!
}
(hasher.finish() as usize) % NEW_BUCKET_COUNT
}
Verdict 1: Атакующий с одного netgroup может заполнить только ограниченное количество bucket'ов. Для полного заполнения нужны адреса из многих /16 подсетей.
2. TRIED Table Manipulation
| # | Attack | Target | Result |
|---|---|---|---|
| 2.1 | Fake successful connections | TRIED table | PROTECTED — mark_good() требует реального TCP соединения |
| 2.2 | Collision в TRIED bucket | Вытеснение honest peers | VULNERABLE — см. анализ ниже |
| 2.3 | Массовые mark_good() | TRIED overflow | PROTECTED — TRIED_BUCKET_COUNT × BUCKET_SIZE = 16384 slots |
Анализ addrman.rs:196-208:
fn mark_good(&mut self, addr: &SocketAddr) {
// ...
let bucket = self.get_tried_bucket(addr);
let pos = self.get_bucket_position(addr, bucket, false);
let idx = bucket * BUCKET_SIZE + pos;
// Handle collision
if let Some(existing_idx) = self.tried_table[idx] {
self.move_to_new(existing_idx); // Existing moved to NEW!
}
self.tried_table[idx] = Some(addr_idx);
}
FINDING [MEDIUM]: При коллизии в TRIED table, существующий адрес перемещается обратно в NEW. Атакующий контролирующий множество IP может вытеснить honest peers из TRIED.
Mitigation: Bucket assignment использует cryptographic hash с secret key, что делает предсказание коллизий вычислительно сложным.
3. Restart Attack (Classic Eclipse)
| # | Attack | Target | Result |
|---|---|---|---|
| 3.1 | Заполнить tables, ждать restart | Все outbound | PROTECTED — full bootstrap always |
| 3.2 | Poisoned peers.dat | Все connections | PROTECTED — full bootstrap overrides |
Анализ startup.rs:79-95:
/// Montana always uses full_bootstrap regardless of chain age:
/// - 100 peers from 25+ subnets
/// - Hardcoded nodes must match network median
pub async fn verify(&self) -> Result<StartupResult, StartupError> {
// Always use full bootstrap for decentralized verification
self.full_bootstrap(chain_age, local_height).await
}
PROTECTED: Montana выполняет полный bootstrap при КАЖДОМ рестарте.
Требования:
- 100 peers (20 hardcoded + 80 P2P)
- 25+ unique /16 subnets
- Hardcoded must match median ±1%
Стоимость атаки: контроль 51+ peers из 25+ /16 подсетей И 15+ hardcoded nodes.
4. Netgroup Diversity Check
| # | Attack | Target | Result |
|---|---|---|---|
| 4.1 | Все outbound из одного /16 | Outbound diversity | PROTECTED |
| 4.2 | Контроль 4+ разных /16 | Bypass diversity | PROTECTED (частично) |
Анализ connection.rs:265-269:
pub async fn can_connect(&self, addr: &SocketAddr) -> bool {
let netgroup = get_netgroup(addr);
let counts = self.netgroup_counts.lock().await;
let current = counts.get(&netgroup).copied().unwrap_or(0);
current < MAX_PEERS_PER_NETGROUP // MAX = 2
}
Verdict: MAX_PEERS_PER_NETGROUP=2 означает атакующему нужно контролировать минимум 4 различных /16 подсети для 8 outbound. Каждая /16 = 65536 IP, это дорого.
5. Eviction Gaming
| # | Attack | Target | Result |
|---|---|---|---|
| 5.1 | Low latency gaming | Eviction protection | PROTECTED — 8 lowest-ping protected |
| 5.2 | Fake tx/slice relay activity | Eviction protection | PROTECTED — 4 recent tx + 4 recent slice protected |
| 5.3 | Netgroup concentration | Eviction target | PROTECTED — evicts from most-concentrated netgroup |
Анализ eviction.rs:63-126:
Eviction защищает по слоям:
- NoBan — trusted peers (4+)
- Netgroup diversity — 4 из разных /16
- Lowest ping — 8 peers
- Recent TX relay — 4 peers
- Recent slice relay — 4 peers
- Longevity — 8 oldest connections
Total protected: 4+8+4+4+8 = 28 peers minimum
С MAX_INBOUND=117 и 28 protected, атакующий может занять 89 slots, но не вытеснить protected diversity peers.
6. Bootstrap Attack
| # | Attack | Target | Result |
|---|---|---|---|
| 6.1 | Контроль DNS seeds | New node bootstrap | PROTECTED — требует 15/20 hardcoded |
| 6.2 | Sybil атака на P2P gossip | Bootstrap peers | PROTECTED — subnet diversity 25+ required |
| 6.3 | Clock manipulation | Time verification | PROTECTED — median from 100 peers |
Анализ bootstrap.rs:37-50:
pub const HARDCODED_NODE_COUNT: usize = 20;
pub const MIN_HARDCODED_RESPONSES: usize = 15;
pub const MIN_DIVERSE_SUBNETS: usize = 25;
pub const BOOTSTRAP_PEER_COUNT: usize = 100;
Security model bootstrap.rs:14-25:
- Требует 15/20 hardcoded nodes согласны
- 25+ уникальных /16 подсетей
- Max 5 peers per subnet
- Hardcoded must match median (1% tolerance)
Verdict: Двойная защита (hardcoded + diversity) требует компрометации И DNS seeds, И 51+ peers из 25+ подсетей.
7. AddrMan Persistence Attack
| # | Attack | Target | Result |
|---|---|---|---|
| 7.1 | Malformed peers.dat | Memory corruption | PROTECTED — 16MB size limit |
| 7.2 | Oversized file DoS | Disk/memory exhaustion | PROTECTED — MAX_ADDRMAN_FILE_SIZE check |
Анализ addrman.rs:107-121:
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, std::io::Error> {
let data = std::fs::read(&path)?;
if data.len() as u64 > MAX_ADDRMAN_FILE_SIZE {
return Err(/* ... */);
}
bincode::deserialize(&data) // Standard bincode
}
FINDING [LOW]: bincode deserialize без explicit limits на collections. При корректном MAX_ADDRMAN_FILE_SIZE это маловероятно, но теоретически возможна memory amplification при crafted data.
Findings Summary
CRITICAL: None
HIGH: None
Montana выполняет полный bootstrap при каждом рестарте (startup.rs:79-95). Атака требует контроля 51+ peers из 25+ /16 подсетей И 15+ hardcoded nodes.
MEDIUM
M-1: TRIED Collision → Move to NEW
| Поле | Значение |
|---|---|
| Файл | addrman.rs:196-208 |
| Описание | При коллизии в TRIED table, existing peer перемещается обратно в NEW вместо отклонения нового |
| Impact | Постепенное вытеснение honest peers из TRIED в менее надёжную NEW table |
| Mitigation | При коллизии сравнивать качество адресов (last_success, attempts) и сохранять лучший |
LOW
L-1: Bincode Deserialize без explicit collection limits
| Поле | Значение |
|---|---|
| Файл | addrman.rs:119, connection.rs:76-77 |
| Описание | bincode::deserialize без serde(deserialize_with) limits на HashMap/Vec |
| Impact | При MAX_FILE_SIZE=16MB атака маловероятна, но возможна memory amplification |
| Mitigation | Добавить explicit size limits через bincode::options().with_limit() |
Checklist Results
[x] Eclipse: full bootstrap on every restart — startup.rs
[x] Eclipse: netgroup diversity — MAX_PEERS_PER_NETGROUP=2
[x] Eclipse: subnet diversity — 25+ /16 subnets required
[x] Eclipse: hardcoded verification — must match median ±1%
[x] Memory: flow control до allocation — MAX_TX_SIZE early check
[x] Memory: все collections bounded — Vec<Option<usize>> fixed size
[x] Slots: eviction защищает diversity — 28 peers protected
[x] Rate: per-IP limiting — MAX_CONNECTIONS_PER_IP=2
Verdict
[x] SAFE — можно продолжать [ ] NEEDS_FIX — исправить перед продолжением
Recommendations
Priority 1: TRIED Collision Resolution
// Вместо безусловного move_to_new:
if let Some(existing_idx) = self.tried_table[idx] {
if let Some(existing) = self.addrs.get(&existing_idx) {
if let Some(new_info) = self.addrs.get(&addr_idx) {
// Сохранить peer с лучшей историей
if existing.last_success > new_info.last_success {
return; // Keep existing, reject new
}
}
}
self.move_to_new(existing_idx);
}
Attack Cost Analysis
Eclipse attack на Montana требует:
| Требование | Стоимость |
|---|---|
| 51+ peers из 25+ /16 подсетей | Контроль IP в 25+ datacenter/ISP |
| 15+ hardcoded DNS seeds | Компрометация инфраструктуры |
| Subnet diversity bypass | Каждая /16 = 65536 IP адресов |
Защита через:
- Full bootstrap при каждом рестарте (100 peers, 25+ subnets)
- Hardcoded verification (must match median ±1%)
- Runtime netgroup diversity (MAX_PEERS_PER_NETGROUP=2)
- Eviction protection (28 peers protected)
Ничто_Nothing_无_金元Ɉ