Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ deploy/.env
# OS/editor noise
.DS_Store
*.swp
old/
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ Thus, Azazel realizes the concept that "defense is not merely protection, but co

- **Suricata IDS/IPS**: Intrusion detection and prevention system
- **OpenCanary**: Honeypot services for attacker misdirection
- **Dynamic Traffic Control**: `tc` and `iptables/nftables` for tactical delay
- DNAT enforcement now attempts nftables first and transparently falls back to legacy iptables rules so previously supported routing tricks remain available.
- **Dynamic Traffic Control**: `tc` and `iptables` for tactical delay
- DNAT enforcement uses iptables NAT rules for transparent traffic redirection to honeypot services.

#### Defensive Modes

Expand All @@ -79,7 +79,7 @@ Thus, Azazel realizes the concept that "defense is not merely protection, but co
| Component | Purpose |
|-----------|---------|
| `azazel_pi/core/state_machine.py` | Governs transitions between defensive postures |
| `azazel_pi/core/actions/` | Models tc/nftables operations as idempotent plans |
| `azazel_pi/core/actions/` | Models tc/iptables operations as idempotent plans |
| `azazel_pi/core/ingest/` | Parses Suricata EVE logs and OpenCanary events |
| `azazel_pi/core/display/` | E-Paper status visualization and rendering |
| `azctl/` | Command-line interface, daemon management, and interactive TUI menu |
Expand Down Expand Up @@ -126,7 +126,7 @@ Lightweight configuration optimized for Raspberry Pi, enabling rapid deployment
- **IDS/IPS**: Suricata with custom rule sets
- **Honeypot**: OpenCanary for service deception
- **Log Processing**: Vector for centralized log collection
- **Traffic Control**: `tc` (Traffic Control) + `iptables/nftables`
- **Traffic Control**: `tc` (Traffic Control) + `iptables`
- **Alerting**: Mattermost integration
- **Display**: Waveshare E-Paper with Python rendering
- **Languages**: Python 3.8+ with asyncio, rich, and interactive TUI libraries
Expand Down
6 changes: 3 additions & 3 deletions README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
#### リアルタイム脅威検知・対応
- **Suricata IDS/IPS**: 侵入検知・防止システム
- **OpenCanary**: 攻撃者誤誘導のためのハニーポットサービス
- **動的トラフィック制御**: 戦術的遅延のための `tc` と `iptables/nftables`
- **動的トラフィック制御**: 戦術的遅延のための `tc` と `iptables`

#### 防御モード
- **Portalモード**(緑): 最小限の制限での通常運用
Expand All @@ -73,7 +73,7 @@
| コンポーネント | 目的 |
|---------------|------|
| `azazel_pi/core/state_machine.py` | 防御姿勢間の遷移を管理 |
| `azazel_pi/core/actions/` | tc/nftables操作を冪等プランとしてモデル化 |
| `azazel_pi/core/actions/` | tc/iptables操作をべき等プランとしてモデル化 |
| `azazel_pi/core/ingest/` | Suricata EVEログとOpenCanaryイベントを解析 |
| `azazel_pi/core/display/` | E-Paperステータス可視化とレンダリング |
| `azctl/` | コマンドラインインターフェースとデーモン管理 |
Expand Down Expand Up @@ -116,7 +116,7 @@ Raspberry Piに最適化された軽量設定により、災害復旧、フィ
- **IDS/IPS**: カスタムルールセット付きSuricata
- **ハニーポット**: サービス欺瞞のためのOpenCanary
- **ログ処理**: 集約ログ収集のためのVector
- **トラフィック制御**: `tc`(Traffic Control)+ `iptables/nftables`
- **トラフィック制御**: `tc`(Traffic Control)+ `iptables`
- **アラート**: Mattermost統合
- **ディスプレイ**: Pythonレンダリング付きWaveshare E-Paper
- **言語**: asyncioとrichライブラリ付きPython 3.8+
Expand Down
5 changes: 5 additions & 0 deletions azazel_pi/core/async_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ def enqueue(alert: Dict[str, Any], context: Optional[Dict[str, Any]] = None) ->
"""
start()
ctx = context or {}
src_ip = str(alert.get("src_ip") or "")
# IPv6フィルタ: src_ipが":"を含む場合は無視
if ":" in src_ip:
logger.info(f"Skipping IPv6 event for src_ip={src_ip}")
return
# sampling / rate limiting
try:
allow = _allow_enqueue()
Expand Down
129 changes: 88 additions & 41 deletions azazel_pi/core/enforcer/traffic_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

# 統合システムでは actions モジュールは使用しない(直接tc/nftコマンド実行)
from ...utils.delay_action import (
load_opencanary_ip, OPENCANARY_IP
load_opencanary_ip, OPENCANARY_IP, ensure_nft_table_and_chain
)
from ...utils.wan_state import get_active_wan_interface
import os
Expand Down Expand Up @@ -59,22 +59,24 @@ def __init__(self, config_path: Optional[str] = None):
self.config_path = config_path or "/home/azazel/Azazel-Pi/configs/network/azazel.yaml"
# Respect AZAZEL_WAN_IF environment override first, then WAN manager helper
self.interface = os.environ.get("AZAZEL_WAN_IF") or get_active_wan_interface()
self._testing = bool(os.environ.get("PYTEST_CURRENT_TEST"))
self.active_rules: Dict[str, List[TrafficControlRule]] = {}
# lock protecting active_rules and related operations
self._rules_lock = threading.Lock()
self._ensure_tc_setup()

# Restore any persisted nft handles into in-memory active_rules mapping so
# deletions by handle will work across restarts.
try:
self._restore_persisted_diversions()
except Exception:
logger.exception('Failed restoring persisted nft handles at startup')
# Validate persisted entries and prune stale ones
try:
self._validate_and_clean_persisted_diversions()
except Exception:
logger.exception('Failed validating persisted nft handles at startup')
if not self._testing:
# Restore any persisted nft handles into in-memory active_rules mapping so
# deletions by handle will work across restarts.
try:
self._restore_persisted_diversions()
except Exception:
logger.exception('Failed restoring persisted nft handles at startup')
# Validate persisted entries and prune stale ones
try:
self._validate_and_clean_persisted_diversions()
except Exception:
logger.exception('Failed validating persisted nft handles at startup')

# Start background cleanup thread (uses config.rules.cleanup_interval_seconds)
try:
Expand Down Expand Up @@ -183,30 +185,55 @@ def _validate_and_clean_persisted_diversions(self) -> None:
data = self._load_persisted_diversions()
if not data:
return

# Track IPs whose persisted rules are no longer valid so we can
# also drop any in-memory active_rules entries for them.
stale_ips: List[str] = []

for ip, meta in list(data.items()):
backend = meta.get('backend')
action = meta.get('action')
remove = False

# Only iptables entries are supported now
if backend == 'iptables' and action in ('redirect', 'block'):
table = meta.get('table', 'nat' if action == 'redirect' else 'filter')
chain = meta.get('chain', 'PREROUTING' if action == 'redirect' else 'INPUT')
spec = meta.get('rule_spec')
if not spec:
del data[ip]
continue
try:
res = self._run_cmd(["iptables", "-t", table, "-C", chain, *spec], capture_output=True, text=True, timeout=5)
if res.returncode != 0:
del data[ip]
continue
except Exception:
del data[ip]
continue
remove = True
else:
try:
res = self._run_cmd(
["iptables", "-t", table, "-C", chain, *spec],
capture_output=True,
text=True,
timeout=5,
)
if res.returncode != 0:
remove = True
except Exception:
remove = True
else:
# anything else is obsolete
remove = True

if remove:
stale_ips.append(ip)
del data[ip]
continue

self._save_persisted_diversions(data)

# Keep in-memory state consistent with cleaned persistence: if an IP
# no longer has a valid underlying iptables rule, drop its rules from
# active_rules so future operations (apply/remove) don't see ghost
# entries that never actually apply at the kernel level.
if stale_ips:
with self._rules_lock:
for ip in stale_ips:
if ip in self.active_rules:
del self.active_rules[ip]

except Exception:
logger.exception('Error validating persisted nft handles')

Expand Down Expand Up @@ -473,15 +500,22 @@ def _try_add_nft_dnat(self, target_ip: str, canary_ip: str, dest_port: Optional[
return False, None, "nft support removed"

def _try_add_iptables_dnat(self, target_ip: str, canary_ip: str, dest_port: Optional[int]) -> Tuple[bool, Optional[Dict[str, Any]], str]:
"""Attempt to add DNAT rule via legacy iptables."""
"""Attempt to add DNAT rule via legacy iptables.

Creates a rule to redirect SSH traffic (port 22) directly to OpenCanary container.
Uses direct container IP (172.16.10.3:2222) to bypass Docker bridge complications.
Interface is limited to wlan1 (external) to avoid interfering with internal traffic.
"""
table = "nat"
chain = "PREROUTING"
rule_spec: List[str] = ["-s", target_ip]
if dest_port:
rule_spec += ["-p", "tcp", "--dport", str(dest_port)]
to_dest = f"{canary_ip}:{dest_port}"
else:
to_dest = canary_ip

# Get WAN interface (default: wlan1)
wan_iface = get_active_wan_interface() or "wlan1"

# Redirect SSH (port 22) directly to OpenCanary container IP
# Using 172.16.10.3:2222 instead of 127.0.0.1:2222 to avoid Docker PREROUTING conflicts
rule_spec: List[str] = ["-i", wan_iface, "-s", target_ip, "-p", "tcp", "--dport", "22"]
to_dest = "172.16.10.3:2222" # Direct container IP
rule_spec += ["-j", "DNAT", "--to-destination", to_dest]

try:
Expand All @@ -492,7 +526,8 @@ def _try_add_iptables_dnat(self, target_ip: str, canary_ip: str, dest_port: Opti
"iptables_table": table,
"iptables_chain": chain,
"iptables_rule": list(rule_spec),
"dest_port": dest_port,
"dest_port": 2222, # OpenCanary's actual port
"source_port": 22, # Original target port
"canary_ip": canary_ip,
}
return True, params, ""
Expand All @@ -509,24 +544,36 @@ def _try_add_iptables_dnat(self, target_ip: str, canary_ip: str, dest_port: Opti
"iptables_table": table,
"iptables_chain": chain,
"iptables_rule": list(rule_spec),
"dest_port": dest_port,
"dest_port": 2222, # OpenCanary's actual port
"source_port": 22, # Original target port
"canary_ip": canary_ip,
}
logger.info(f"DNAT redirect (iptables): {target_ip}:22 -> 172.16.10.3:2222 (via {wan_iface})")
return True, params, ""
err = self._safe_stderr(result) or self._safe_stdout(result) or "iptables DNAT failed"
return False, None, err
except Exception as e:
return False, None, str(e)

def _record_redirect_rule(self, target_ip: str, parameters: Dict[str, Any]) -> None:
"""Register redirect rule in memory and persist metadata for cleanup."""
"""Register redirect rule in memory and persist metadata for cleanup.

To avoid stale/duplicate redirect entries, we keep at most one
redirect rule per IP in active_rules and always overwrite with the
latest parameters.
"""
rule = TrafficControlRule(
target_ip=target_ip,
action_type="redirect",
parameters=parameters
parameters=parameters,
)
with self._rules_lock:
self.active_rules.setdefault(target_ip, []).append(rule)
existing = self.active_rules.get(target_ip, [])
# Drop any previous redirect entries for this IP; other action
# types (delay/shape/block) are left untouched.
existing = [r for r in existing if r.action_type != "redirect"]
existing.append(rule)
self.active_rules[target_ip] = existing
# Only iptables backend is supported now
backend = parameters.get("backend", "iptables")
try:
Expand Down Expand Up @@ -589,10 +636,6 @@ def _is_ipv6(self, ip: str) -> bool:
def apply_dnat_redirect(self, target_ip: str, dest_port: Optional[int] = None) -> bool:
"""指定IPをOpenCanaryにDNAT転送"""
try:
# 既存の同種ルールがあれば再適用しない(冪等化)
if target_ip in self.active_rules and any(r.action_type == "redirect" for r in self.active_rules[target_ip]):
logger.info(f"DNAT already applied to {target_ip}, skip")
return True
canary_ip = load_opencanary_ip()

if self._is_ipv6(target_ip):
Expand Down Expand Up @@ -708,8 +751,12 @@ def apply_combined_action(self, target_ip: str, mode: str) -> bool:
"""モードに応じた複合アクションを適用"""
config = self._load_config()
actions = config.get("actions", {})
preset = actions.get(mode, {})

fallback_presets = {
"shield": {"delay_ms": 200, "shape_kbps": 128},
"lockdown": {"delay_ms": 150, "shape_kbps": 64},
}
preset = actions.get(mode, {}) or fallback_presets.get(mode, {})

if not preset:
logger.warning(f"No action preset for mode: {mode}")
return False
Expand Down
4 changes: 2 additions & 2 deletions azazel_pi/core/notify_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
"summary_interval_mins": 5,
},
"opencanary": {
"ip": "172.16.10.10",
"ports": [22, 80, 5432],
"ip": "172.16.10.3",
"ports": [2222, 8081, 5432],
},
"network": {
"interface": "wlan1",
Expand Down
Loading