6ヶ月前、私はメインのデバッグツールとしてWiresharkを使うのをやめ、ほぼ完全にScapyへと移行しました。Wiresharkを批判しているわけではありません——今もこの瞬間、別のタブで起動しています——しかし、パッシブなキャプチャツールには絶対にできないことをScapyが実現できると分かった時、ネットワークのトラブルシューティングへのアプローチが根本的に変わりました。
Scapyはパケットをゼロから構築し、送信し、レスポンスをキャプチャして、すべてをPythonで解析できます。ただ観察するだけでなく、能動的に構築して注入できるのです。「トラフィックを見られる」と「任意のトラフィックを生成できる」の差は計り知れないほど大きいです。特に、誤動作しているファイアウォールルールをデバッグしたり、サーバーが不正な入力に対して正しく応答するかどうかを確認したりする場合はなおさらです。
クイックスタート — 5分で動かす
仮想環境の中にScapyをインストールします。rawソケットへのアクセスにはrootまたはAdministrator権限が必要です:
python3 -m venv scapy-env
source scapy-env/bin/activate
pip install scapy
インタラクティブシェルを開いて素早くテストする:
sudo scapy
初めてのICMP pingを送信してレスポンスを受け取る:
from scapy.all import *
# シンプルなICMPエコーリクエストを構築
packet = IP(dst="8.8.8.8") / ICMP()
response = sr1(packet, timeout=2, verbose=False)
if response:
print(f"{response.src} から応答を受信、TTL={response.ttl}")
else:
print("応答なし")
これがScapyのメンタルモデル全体です、たった3行で:/でレイヤーを構築し、sr1()(送信+1つ受信)で送り、結果を確認する。すべてのプロトコルスタックは単にレイヤーを重ねたものです。
パケットレイヤー、スニッフィング、PCAPファイル
レイヤースタッキングの仕組み
Scapyの各プロトコルはクラスです。/演算子でスタックすると、指定しないフィールドにはScapyが適切なデフォルト値を自動的に入力します:
from scapy.all import *
# レイヤーが持つフィールドを確認
ls(TCP)
# Ethernet + IP + TCP + HTTPライクなペイロードを構築
pkt = Ether() / IP(dst="192.168.1.1") / TCP(dport=80, flags="S") / Raw(load="GET / HTTP/1.0\r\n\r\n")
# パケットの完全な内訳を表示
pkt.show()
pkt.show()を実行すると、チェックサム、送信元MAC、シーケンス番号など、すべてのフィールドが表示されます。このコマンド一つで、アドレス書き換え後にチェックサムを再計算していないNATデバイスによってパケットが静かに破損されていたプロジェクトで、何時間もの時間を節約できました。
ライブトラフィックのスニッフィング
Scapyはパケットのキャプチャもでき、すべてのフィールドにPythonで完全にアクセスできます:
from scapy.all import sniff, IP, TCP
def analyze_packet(pkt):
if pkt.haslayer(IP) and pkt.haslayer(TCP):
src = pkt[IP].src
dst = pkt[IP].dst
dport = pkt[TCP].dport
flags = pkt[TCP].flags
print(f"{src} -> {dst}:{dport} [{flags}]")
# eth0で20個のTCPパケットをキャプチャ
sniff(iface="eth0", filter="tcp", prn=analyze_packet, count=20)
BPFフィルター構文はtcpdumpと完全に一致します——tcpdumpフィルターを書いたことがあれば、変更なしでそのまま使えます。本当の違いはコールバックです。完全なPythonが使えるため、マッチしたパケットをデータベースに書き込んだり、アラートをトリガーしたり、変更して再注入したり、ワークフローで使う他のツールと連携させたりできます。
PCAPファイルの読み書き
from scapy.all import rdpcap, wrpcap
# 既存のキャプチャファイルを読み込む
packets = rdpcap("capture.pcap")
# 特定のパケットを確認
for pkt in packets:
if pkt.haslayer("DNS"):
print(pkt["DNS"].qd.qname)
# フィルタリングされたパケットを新しいファイルに書き込む
dns_packets = [p for p in packets if p.haslayer("DNS")]
wrpcap("dns_only.pcap", dns_packets)
高度な使い方 — プロトコルテストとネットワークシミュレーション
TCPハンドシェイクテスト
私が最もよく使うスクリプトの一つは、特定のポートが開いているかどうかをテストし、SYN/SYN-ACKの正確なラウンドトリップ時間を計測するものです。Pingはホストが到達可能かどうかを教えてくれます。これはサービスが実際に応答しているかどうか——そしてどれほど速く——を教えてくれます:
from scapy.all import *
import time
def test_tcp_port(host, port):
syn = IP(dst=host) / TCP(dport=port, sport=RandShort(), flags="S", seq=1000)
start = time.time()
response = sr1(syn, timeout=3, verbose=False)
elapsed = (time.time() - start) * 1000
if response is None:
return f"{host}:{port} — 応答なし(フィルター済み)"
tcp_resp = response[TCP]
if tcp_resp.flags == 0x12: # SYN-ACK
return f"{host}:{port} — オープン、RTT={elapsed:.1f}ms"
elif tcp_resp.flags == 0x14: # RST-ACK
return f"{host}:{port} — クローズド"
else:
return f"{host}:{port} — 予期しないフラグ: {tcp_resp.flags}"
print(test_tcp_port("192.168.1.1", 22))
print(test_tcp_port("192.168.1.1", 443))
ラボテスト用のARPスプーフィング
ARPスプーフィングは、ネットワークセグメンテーションをテストする際に、制御されたラボ環境で再現するのに最も有用なことの一つです。パケットレベルで攻撃がどのように見えるかを正確に理解していれば、スイッチが実際にそれをブロックするかどうかの確認が簡単になります。明示的な認可がある隔離された環境でのみ実行してください:
from scapy.all import Ether, ARP, sendp
import time
def arp_spoof(target_ip, spoof_ip, iface="eth0"):
"""
ターゲットにGratuitous ARPを送信し、自分がspoof_ipであると主張する。
ラボ専用 — 認可が必要。
"""
# まずターゲットのMACアドレスを取得
target_mac = getmacbyip(target_ip)
if not target_mac:
print(f"{target_ip} のMACアドレスを解決できませんでした")
return
# ARPリプライを構築
pkt = Ether(dst=target_mac) / ARP(
op=2, # ARPリプライ
pdst=target_ip,
hwdst=target_mac,
psrc=spoof_ip # このIPになりすます
)
print(f"スプーフィングARPを送信中: {spoof_ip} は自分のMACにある -> {target_ip}")
sendp(pkt, iface=iface, verbose=False)
# 例: 192.168.1.2がゲートウェイIPからのスプーフィングARPを受け入れるかテスト
arp_spoof("192.168.1.2", "192.168.1.1")
送信後、ターゲットでarp -nを使ってARPテーブルを確認します。Dynamic ARP Inspectionがスイッチに適切に設定されていれば、パケットは到達する前にドロップされます。ターゲットのARPテーブルが実際に変更されていた場合——他の誰かが見つける前に修正すべき脆弱性が存在します。
DNSクエリファジング
標準的なDNSクライアントでは、クエリのワイヤーフォーマットに何が入るかを制御できません。Scapyなら可能です:
from scapy.all import IP, UDP, DNS, DNSQR, sr1
def dns_query(server, name, qtype="A"):
query = IP(dst=server) / UDP(dport=53) / DNS(
rd=1,
qd=DNSQR(qname=name, qtype=qtype)
)
resp = sr1(query, timeout=3, verbose=False)
if resp and resp.haslayer(DNS):
dns = resp[DNS]
print(f"レスポンスコード: {dns.rcode}")
if dns.ancount > 0:
for i in range(dns.ancount):
print(f" 回答: {dns.an[i].rdata}")
else:
print("DNSレスポンスを受信できませんでした")
dns_query("8.8.8.8", "example.com")
dns_query("8.8.8.8", "example.com", qtype="MX")
6ヶ月の日常使用から得た実践的なヒント
スクリプトでは常にverbose=Falseを設定する
デフォルトの出力はインタラクティブには問題ありません。自動化されたスクリプトではログが溢れてしまいます。インタラクティブシェルの外では、すべてのsend()、sr1()、srp()の呼び出しにverbose=Falseを追加してください。
デフォルトインターフェースにconf.ifaceを使う
from scapy.all import conf
# スクリプトの先頭で一度設定する
conf.iface = "eth0"
# 以降のすべての送信でこのインターフェースが自動的に使用される
sendpfast()による並列送信
負荷テストには、sendpfast()が内部でtcpreplayを使用します。send()をループするよりも大幅に速いです——リンクを飽和させたり、ファイアウォールのパケット処理をストレステストしようとするときに、この差は重要になります:
from scapy.all import sendpfast, IP, ICMP
packets = [IP(dst=f"192.168.1.{i}") / ICMP() for i in range(1, 255)]
sendpfast(packets, mbps=10, loop=2, iface="eth0")
BPFとPythonによるフィルタリング
BPFフィルター(filter=引数)は、パケットがPythonに到達する前にカーネルレベルで実行されます。パフォーマンスが重要なものにはこれを使います。BPFでは表現できないロジック——ペイロードのコンテンツマッチング、複数パケットにまたがるステートフル解析など——にはPythonサイドのフィルタリングを使います。
チェックサムに注意する
Scapyは新しいパケットを送信するときにチェックサムを自動的に再計算します。しかし、PCAPから読み込んで再生する場合は、チェックサムフィールドを明示的に削除して再計算を強制します:
del pkt[IP].chksum
del pkt[TCP].chksum
# 次のアクセスまたは送信時にScapyが再計算する
これを省略したせいで半日を無駄にしました。壊れたファイアウォールのように見えるものを追いかけていたのですが、実際にはリプレイスクリプトから送られた無効なチェックサムを持つパケットを、ファイアウォールが正しくドロップしていただけだったのです。
tmuxまたはscreenの中で実行する
長いキャプチャやフラッドテストはデタッチされたセッションで行うべきです。テスト中にSSH接続が切断されると、PCAPファイルが破損し、rawソケットが開いたままになります。1分以上かかるものを開始する前には、必ずtmux new -s scapy-testにアタッチします。
6ヶ月を経て、ネットワーク問題へのアプローチが変わったことを実感しています。何かが予期しない動作をする時、アプリケーション層の変更を通じて適切な条件を引き出し、望みのパケットが現れることを期待するようなことはもうありません。欲しいパケットを構築して送信するだけです。それが本質的な違いです——症状を追いかけるのをやめ、直接原因へ向かうのです。

