はじめに
ネットワーク管理者が最も恐れるものの一つ、それがループだ。
今回は、ESP32 と 2 枚の W5500 を使い、ポート A とポート B が同一セグメント(直結)されているかを、ネットワークを破壊する前に検知する「ループ未然検知ツール」を Rust で作成した。
その開発過程で遭遇した W5500 の「物理仕様の罠」と、それをいかに克服したかの記録を共有する。
ソースコード (GitHub)
詳細な実装や sdkconfig の設定は、こちらのリポジトリを参考にしてくれ。
前回はLLMの言うがままにsmoltcpとcotton_w5500を使ったが
今回はw5500_hlをつかう。UDPブロードキャストを使うからだ。ARPは使わない。
検証時の様子
完成時の様子
🛠️ 今回の開発を支えるコア技術:未知のネットワークを暴く「三段構え」の設計
「未知のネットワークに繋ぐ」という過酷な要件をクリアするために実装した、本作の心臓部と言える技術を深掘りしていくぜ。
1. 🚀 UDPブロードキャスト:セグメントを横断する「デジタル指紋」
今回のツールの核心は
「Port Aから投げた叫び(パケット)を、Port Bが聞き取れるか?」
というシンプルな導通テストを、UDPブロードキャストという手法で実現している点にある。
なぜ「UDPブロードキャスト」なのか?
通常、特定の相手と通信するにはIPアドレスを知る必要があるが、未知のネットワークでは相手の居場所がわからない。そこで以下の特性を利用した。
-
全方位への射出: UDPブロードキャスト(
255.255.255.255宛て)を使うことで、セグメント内に存在するすべての機器に対してパケットを届けることができる。 - L2レベルの透過性: スイッチングハブはブロードキャストパケットをすべてのポートへ転送(フラッディング)する。これを利用することで、物理的な配線がどう入り組んでいても、道が繋がってさえいればPort Bにパケットが届く。
-
未知のセグメントへの適応: APIPA(
169.254.x.x)を固定で割り当てることで、DHCPサーバが存在しない現場でもL2レベルでの導通を即座に確認できるのが強みだ。
「MAGICパケット」による確実な判定
単にパケットが届くだけでは、それが自分の投げたものか、既存ネットワークのノイズなのか区別がつかない。そこで導入したのが「デジタル指紋(MAGICパケット)」だ。
-
独自の識別子:
CHECK_LOOPという特定の文字列を含んだUDPパケットを、送信側(Port 8887)から受信側(Port 8888)へ向けて射出する。 - 偽陽性の排除: 受信側は届いたパケットの内容を厳格に検証し、自身のMAGIC文字列と一致した場合のみ「ループ」と判定する。これにより、他人のパケットを「ループ」と誤認するリスクをゼロにしているぜ。
パケットの内容は付録を参照 wiresharkのキャプチャを掲載している
「未然」に防ぐ:ストームが起きる前の警告
本ツールの真骨頂は、本格的な「ブロードキャストストーム」が起きてネットワークが沈黙する前に、「今この2点を繋ぐと危ないぞ」と教えてくれる点にある。
- 低負荷なプローブ: 毎秒1回という控えめな頻度でパケットを投げるため、既存の通信を圧迫することなく安全に診断できる。
- 物理リンクとの連動: ケーブルを挿した直後にLED(赤/緑)を確認するだけで、そのポートの安全性を即座に判断できる設計だ。
2. LwIPバイパス:ネットワークスタックの「完全分離」
ESP32(ESP-IDF)には標準で強力なネットワークスタック「LwIP」が搭載されている。しかし、本プロジェクトではあえてそれを通さず、W5500を直接SPIで制御する設計を選択した。
- ルーティング競合の回避: 通常、同じサブネットを持つ2つのLANポートをOS(LwIP)に認識させると、パケットをどちらのポートから出すべきかOS側が混乱し、意図した監視ができなくなる。
- 「物理デバイス」としての制御: W5500をOSの管理下から切り離し、独立した「SPIデバイス」として扱うことで、標準スタックの常識を超えた自由なパケット操作を実現した。
- ハードウェア・オフロードの最大活用: UDPパケットの生成やチェックサム計算をW5500内部のハードウェアで行うため、ESP32側のCPU負荷を極限まで抑えつつ、10ms単位の高速なポーリングを維持できるぜ。
3. Rustの所有権モデルによるSPIバス共有
1本のSPIバスを2枚のW5500で共有し、かつ20MHzという高速クロックで回す際、最も恐ろしいのがチップセレクト(CS)の競合によるデータの混線だ。
-
型安全な排他制御: Rustの
esp-idf-halが提供するSPIドライバは、Rustの「所有権システム」に基づいて設計されている。 -
暗黙のCS切り替え:
SpiDeviceDriverをUnit AとBそれぞれにインスタンス化することで、コード上でチップを切り替える際、物理的なCSピンの制御がハードウェアレベルで安全に行われる。 - 「競合」をコンパイルタイムで防ぐ: 万が一、同時に2つのチップを操作しようとするコードを書けば、Rustのコンパイラがエラーを出す。「絶対に間違えられない仕組み」が、複雑なダブルW5500構成の安定性を支えているんだ。
🛠️ 乗り越えた壁:ゾンビソケット現象との死闘
開発終盤、奇妙な現象に悩まされた。ループを検知してしばらく放置すると、物理リンク LED は緑(UP)なのに、パケットの受信が完全に止まるのだ。
ログが語る真実
独自に実装したテレメトリログから、驚くべき事実が判明した。
W: Unit B: リンクダウン
I: SOCK(B) udp=0 closed=50
物理ケーブルは一切触っていない。それなのに、W5500内部のソケット状態が勝手に Closed (0x00) に落ちていた。
原因は、接続先のL2スイッチにある「ループ防止機能」だった。ツールがループを検知させることでスイッチ側がポートを一時遮断し、その際の瞬断(マイクロ・リンクダウン)によってW5500のソケットが強制クローズされていたのだ。これが「ゾンビソケット」の正体だ。
4. 実装:ステートマシンによる「自己修復(Self-Healing)」
この課題に対し、「ソケットの健康診断と自動再バインド」 という設計で対抗した。今回の開発で最大の武器となったロジックだ。
-
「ゾンビ化」の即時検知: モニタリング用のステートマシンが毎ループ、W5500内部のステータスレジスタ(
Sn_SR)を直接チェックする。 -
人間が気づく前に再生する: ソケットが
Closedになっていることを検知した瞬間に、即座にudp_bindを再発行して「耳」を開き直す。 - 「待ち伏せ」の成功: この自己修復により、スイッチ側がポートの遮断を解除したコンマ数秒後には、デバイスは既に受信待ち状態で待機できている。このレジリエンス(回復力)が、現場での「確実な検知」を可能にしたぜ。
💡 まとめ:なぜこれが「最強」の診断ツールなのか
多くの市販ネットワークツールはOS上のスタックに依存しているため、一度リンクが切れると再構築に数秒〜数十秒の時間を要する。
それに対し、本ツールは 「APIPAによるIP自動割り当て」「直接SPI制御によるOSバイパス」「レジスタ監視による自己修復」 を組み合わせることで、マイクロ秒単位の瞬断を乗り越え、未知のネットワークを秒速で診断するという、唯一無二の性能を手に入れたんだ。
糸冬了!!
付録1:Rustコード
use std::thread;
use std::time::{Duration, Instant};
use esp_idf_hal::{
gpio::{Output, PinDriver},
peripherals::Peripherals,
spi::{self, SpiDeviceDriver, SpiDriver},
units::Hertz,
};
use w5500_hl::{
ll::{
eh1::vdm::W5500,
net::{Eui48Addr, Ipv4Addr, SocketAddrV4},
LinkStatus, Registers, Sn, SocketStatus,
},
Error as HlError, Udp,
};
const SOCKET: Sn = Sn::Sn0;
const SPI_BAUDRATE_HZ: u32 = 20_000_000;
const SEND_PORT: u16 = 8887;
const CHECK_PORT: u16 = 8888;
const MAGIC: &[u8] = b"CHECK_LOOP";
const MAC_A: Eui48Addr = Eui48Addr { octets: [0x00, 0x08, 0xDC, 0x01, 0x00, 0x01] };
const MAC_B: Eui48Addr = Eui48Addr { octets: [0x00, 0x08, 0xDC, 0x01, 0x00, 0x02] };
const IP_A: Ipv4Addr = Ipv4Addr::new(169, 254, 1, 1);
const IP_B: Ipv4Addr = Ipv4Addr::new(169, 254, 1, 2);
const SUBNET: Ipv4Addr = Ipv4Addr::new(255, 255, 0, 0);
const GW_A: Ipv4Addr = Ipv4Addr::new(169, 254, 1, 2);
const GW_B: Ipv4Addr = Ipv4Addr::new(169, 254, 1, 1);
const BCAST: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::new(255, 255, 255, 255), CHECK_PORT);
const PHY_INTERVAL: Duration = Duration::from_millis(500);
const SEND_INTERVAL: Duration = Duration::from_millis(1_000);
const BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAIN_LOOP_SLEEP: Duration = Duration::from_millis(10);
type OutputPin<'a> = PinDriver<'a, Output>;
#[derive(Clone, Copy, PartialEq)]
enum State { Normal, NoLink, LoopDetected }
struct MonitorState {
state: State,
loop_detected: bool,
link_a_up: bool,
link_b_up: bool,
last_send: Instant,
last_phy_check: Instant,
last_blink: Instant,
blink_on: bool,
}
impl MonitorState {
fn new() -> Self {
Self {
state: State::Normal,
loop_detected: false,
link_a_up: false,
link_b_up: false,
last_send: Instant::now() - SEND_INTERVAL,
last_phy_check: Instant::now() - PHY_INTERVAL,
last_blink: Instant::now(),
blink_on: false,
}
}
fn poll_links<WA: Registers + Udp, WB: Registers + Udp>(
&mut self,
w5500_a: &mut WA,
w5500_b: &mut WB,
) {
if self.last_phy_check.elapsed() < PHY_INTERVAL {
return;
}
let previous_a = self.link_a_up;
let previous_b = self.link_b_up;
self.link_a_up = read_link(w5500_a, "A");
self.link_b_up = read_link(w5500_b, "B");
Self::log_link_transition("A", previous_a, self.link_a_up);
Self::log_link_transition("B", previous_b, self.link_b_up);
if !previous_a && self.link_a_up {
let _ = Self::rebind_socket(w5500_a, SEND_PORT, "A");
}
if !previous_b && self.link_b_up {
let _ = Self::rebind_socket(w5500_b, CHECK_PORT, "B");
}
if !self.link_a_up || !self.link_b_up {
self.loop_detected = false;
}
self.last_phy_check = Instant::now();
}
fn send_probe<W: Registers + Udp>(&mut self, w5500_a: &mut W) {
if self.last_send.elapsed() < SEND_INTERVAL {
return;
}
if self.link_a_up {
if !Self::ensure_socket_ready(w5500_a, SEND_PORT, "A") {
self.last_send = Instant::now();
return;
}
match w5500_a.udp_send_to(SOCKET, MAGIC, &BCAST) {
Ok(n) => log::debug!("A→BC 送信: {n} bytes"),
Err(_) => log::warn!("送信エラー"),
}
}
self.last_send = Instant::now();
}
fn receive_loopback<W: Registers + Udp>(&mut self, w5500_b: &mut W, recv_buf: &mut [u8; 32]) {
if !Self::ensure_socket_ready(w5500_b, CHECK_PORT, "B") {
return;
}
match w5500_b.udp_recv_from(SOCKET, recv_buf) {
Ok((n, src)) => {
let n = n as usize;
if is_magic_packet(&recv_buf[..n], MAGIC) {
if !self.loop_detected {
let [a, b, c, d] = src.ip().octets();
log::info!("ループ検知: 送信元 {a}.{b}.{c}.{d}:{}", src.port());
self.loop_detected = true;
}
} else {
log::debug!("B: 受信 {n} bytes (MAGIC 不一致)");
}
}
Err(HlError::WouldBlock) => {}
Err(_) => log::warn!("受信エラー"),
}
}
fn ensure_socket_ready<W: Registers + Udp>(w5500: &mut W, port: u16, label: &str) -> bool {
match w5500.sn_sr(SOCKET) {
Ok(Ok(SocketStatus::Udp)) => true,
Ok(status) => {
log::warn!("Unit {label}: ソケット状態={status:?} のため再bind");
Self::rebind_socket(w5500, port, label)
}
Err(_) => {
log::warn!("Unit {label}: ソケット状態読み出し失敗");
false
}
}
}
fn rebind_socket<W: Udp>(w5500: &mut W, port: u16, label: &str) -> bool {
if w5500.udp_bind(SOCKET, port).is_ok() {
log::info!("Unit {label}: UDPソケット再bind成功 port={port}");
true
} else {
log::warn!("Unit {label}: UDPソケット再bind失敗 port={port}");
false
}
}
fn log_link_transition(label: &str, previous: bool, current: bool) {
if previous == current {
return;
}
if current {
log::info!("Unit {label}: リンクアップ");
} else {
log::warn!("Unit {label}: リンクダウン");
}
}
fn compute_display_state(&self) -> State {
match (self.link_a_up && self.link_b_up, self.loop_detected) {
(false, _) => State::NoLink,
(true, true) => State::LoopDetected,
(true, false) => State::Normal,
}
}
fn refresh_display_state(&mut self) -> bool {
let new_state = self.compute_display_state();
if new_state == self.state {
return false;
}
self.state = new_state;
if self.state == State::NoLink {
self.blink_on = false;
self.last_blink = Instant::now();
}
true
}
fn should_blink(&self) -> bool {
self.state == State::NoLink && self.last_blink.elapsed() >= BLINK_INTERVAL
}
fn toggle_blink(&mut self) -> bool {
self.blink_on = !self.blink_on;
self.last_blink = Instant::now();
self.blink_on
}
}
struct StatusLeds<'a> {
green: OutputPin<'a>,
red: OutputPin<'a>,
}
impl<'a> StatusLeds<'a> {
fn new(green: OutputPin<'a>, red: OutputPin<'a>) -> Self {
Self { green, red }
}
fn turn_off(&mut self) -> bool {
let green_ok = set_led_level(&mut self.green, false, "緑");
let red_ok = set_led_level(&mut self.red, false, "赤");
green_ok && red_ok
}
fn apply_state(&mut self, state: State) -> bool {
match state {
State::Normal => {
set_led_level(&mut self.green, true, "緑")
&& set_led_level(&mut self.red, false, "赤")
}
State::LoopDetected => {
set_led_level(&mut self.green, false, "緑")
&& set_led_level(&mut self.red, true, "赤")
}
State::NoLink => self.turn_off(),
}
}
fn apply_blink(&mut self, on: bool) {
let _ = set_led_level(&mut self.green, on, "緑");
let _ = set_led_level(&mut self.red, on, "赤");
}
}
fn main() {
initialize_runtime();
log::info!("=== ループ未然検知セグメントチェッカー 起動 ===");
let Ok(peripherals) = Peripherals::take() else {
log::error!("Peripherals の取得に失敗");
return;
};
let pins = peripherals.pins;
let mut rst = match PinDriver::output(pins.gpio32) {
Ok(pin) => pin,
Err(e) => {
log::error!("RST ピン初期化失敗: {e:?}");
return;
}
};
if !pulse_reset_line(&mut rst) {
return;
}
// Initialize status LEDs to the off state before entering the monitor loop.
let led_green = match PinDriver::output(pins.gpio18) {
Ok(pin) => pin,
Err(e) => {
log::error!("緑 LED ピン初期化失敗: {e:?}");
return;
}
};
let led_red = match PinDriver::output(pins.gpio19) {
Ok(pin) => pin,
Err(e) => {
log::error!("赤 LED ピン初期化失敗: {e:?}");
return;
}
};
let mut leds = StatusLeds::new(led_green, led_red);
if !leds.turn_off() {
return;
}
let spi_driver = match SpiDriver::new(
peripherals.spi2,
pins.gpio14,
pins.gpio27,
Some(pins.gpio33),
&spi::SpiDriverConfig::new(),
) {
Ok(driver) => driver,
Err(e) => {
log::error!("SPI ドライバ初期化失敗: {e:?}");
return;
}
};
let cfg = spi::config::Config::new().baudrate(Hertz(SPI_BAUDRATE_HZ));
let mut w5500_a = match SpiDeviceDriver::new(&spi_driver, Some(pins.gpio25), &cfg) {
Ok(dev) => W5500::new(dev),
Err(e) => {
log::error!("W5500 A SPI デバイス作成失敗: {e:?}");
return;
}
};
let mut w5500_b = match SpiDeviceDriver::new(&spi_driver, Some(pins.gpio13), &cfg) {
Ok(dev) => W5500::new(dev),
Err(e) => {
log::error!("W5500 B SPI デバイス作成失敗: {e:?}");
return;
}
};
if !configure_w5500_pair(&mut w5500_a, &mut w5500_b) {
return;
}
if !bind_udp_sockets(&mut w5500_a, &mut w5500_b) {
return;
}
log::info!("SPI baudrate={} Hz", SPI_BAUDRATE_HZ);
// Hand off to the steady-state monitor loop after startup succeeds.
run_monitor_loop(&mut w5500_a, &mut w5500_b, &mut leds);
}
// Initialize the ESP-IDF runtime hooks and default logger once at startup.
fn initialize_runtime() {
esp_idf_svc::sys::link_patches();
esp_idf_svc::log::EspLogger::initialize_default();
}
// Run the normal operating loop once all hardware initialization has completed.
fn run_monitor_loop<WA: Registers + Udp, WB: Registers + Udp>(
w5500_a: &mut WA,
w5500_b: &mut WB,
leds: &mut StatusLeds<'_>,
) {
// Keep monitor state and timers together so the loop reads as a sequence of actions.
let mut monitor = MonitorState::new();
let mut recv_buf = [0u8; 32];
loop {
// Refresh physical link status and clear loop state when either side drops.
monitor.poll_links(w5500_a, w5500_b);
// Emit one probe packet from unit A on the configured interval.
monitor.send_probe(w5500_a);
// Sample one inbound packet on unit B and latch loop detection when it matches.
monitor.receive_loopback(w5500_b, &mut recv_buf);
// Update the steady LED pattern only when the aggregate display state changes.
if monitor.refresh_display_state() {
let _ = leds.apply_state(monitor.state);
}
// Blink both LEDs while waiting for both links to come up.
if monitor.should_blink() {
leds.apply_blink(monitor.toggle_blink());
}
thread::sleep(MAIN_LOOP_SLEEP);
}
}
// Toggle the shared reset line once so both W5500 chips start from a known state.
fn pulse_reset_line(rst: &mut OutputPin<'_>) -> bool {
if let Err(e) = rst.set_low() {
log::error!("RST Low 出力失敗: {e:?}");
return false;
}
thread::sleep(Duration::from_millis(100));
if let Err(e) = rst.set_high() {
log::error!("RST High 出力失敗: {e:?}");
return false;
}
thread::sleep(Duration::from_millis(500));
true
}
// Configure both W5500 chips with their fixed network identities and verify access.
fn configure_w5500_pair<WA: Registers, WB: Registers>(w5500_a: &mut WA, w5500_b: &mut WB) -> bool {
if init_w5500(w5500_a, &MAC_A, &IP_A, &SUBNET, &GW_A, "A").is_err() {
log::error!("W5500 A 初期化失敗");
return false;
}
if init_w5500(w5500_b, &MAC_B, &IP_B, &SUBNET, &GW_B, "B").is_err() {
log::error!("W5500 B 初期化失敗");
return false;
}
true
}
// Bind the fixed UDP sockets used for probe transmit and loopback receive.
fn bind_udp_sockets<WA: Udp, WB: Udp>(w5500_a: &mut WA, w5500_b: &mut WB) -> bool {
if w5500_a.udp_bind(SOCKET, SEND_PORT).is_err() {
log::error!("Unit A bind 失敗");
return false;
}
if w5500_b.udp_bind(SOCKET, CHECK_PORT).is_err() {
log::error!("Unit B bind 失敗");
return false;
}
log::info!("Unit A bind port={SEND_PORT}, Unit B bind port={CHECK_PORT}");
true
}
// Drive one LED high or low and log any failure with its label.
fn set_led_level(led: &mut OutputPin<'_>, high: bool, label: &str) -> bool {
let result = if high { led.set_high() } else { led.set_low() };
if let Err(e) = result {
let level = if high { "High" } else { "Low" };
log::warn!("{label} LED {level} 失敗: {e:?}");
return false;
}
true
}
// Read PHY link state and treat any read failure as link-down for safety.
fn read_link<W: Registers>(w5500: &mut W, label: &str) -> bool {
match w5500.phycfgr() {
Ok(phy) => phy.lnk() == LinkStatus::Up,
Err(_) => {
log::warn!("Unit {label}: PHY 読み出し失敗");
false
}
}
}
// Match only packets that begin with the loop-check magic bytes.
fn is_magic_packet(payload: &[u8], magic: &[u8]) -> bool {
payload.starts_with(magic)
}
fn init_w5500<W: Registers>(
w5500: &mut W,
mac: &Eui48Addr,
ip: &Ipv4Addr,
subnet: &Ipv4Addr,
gateway: &Ipv4Addr,
label: &str,
) -> Result<(), W::Error> {
w5500.set_shar(mac)?;
w5500.set_sipr(ip)?;
w5500.set_subr(subnet)?;
w5500.set_gar(gateway)?;
let [a, b, c, d] = ip.octets();
let ver = w5500.version()?;
if ver == w5500_hl::ll::VERSION {
log::info!("W5500 {label} OK IP={a}.{b}.{c}.{d} ver=0x{ver:02X}");
} else {
log::warn!("W5500 {label} 不明バージョン 0x{ver:02X} (期待: 0x04)");
}
Ok(())
}
付録2:Wiresharkのログ
Frame 188515: Packet, 60 bytes on wire (480 bits), 60 bytes captured (480 bits) on interface \Device\NPF_{A2E552EF-F3D5-4652-91A0-E71C7DB54E11}, id 0
Section number: 1
Interface id: 0 (\Device\NPF_{A2E552EF-F3D5-4652-91A0-E71C7DB54E11})
Interface name: \Device\NPF_{A2E552EF-F3D5-4652-91A0-E71C7DB54E11}
Interface description: Wi-Fi
Encapsulation type: Ethernet (1)
Arrival Time: May 6, 2026 12:48:00.866871900 東京 (標準時)
UTC Arrival Time: May 6, 2026 03:48:00.866871900 UTC
Epoch Arrival Time: 1778039280.866871900
[Time shift for this packet: 0.000000000 seconds]
[Time delta from previous captured frame: 167.382600 milliseconds]
[Time delta from previous displayed frame: 1.024008700 seconds]
[Time since reference or first frame: 1 hour, 29 minutes, 19.050344200 seconds]
Frame Number: 188515
Frame Length: 60 bytes (480 bits)
Capture Length: 60 bytes (480 bits)
[Frame is marked: False]
[Frame is ignored: False]
[Protocols in frame: eth:ethertype:ip:udp:data]
Character encoding: ASCII (0)
[Coloring Rule Name: UDP]
[Coloring Rule String: udp]
Ethernet II, Src: 82:80:58:01:00:01 (82:80:58:01:00:01), Dst: Broadcast (ff:ff:ff:ff:ff:ff)
Destination: Broadcast (ff:ff:ff:ff:ff:ff)
.... ..1. .... .... .... .... = LG bit: Locally administered address (this is NOT the factory default)
.... ...1 .... .... .... .... = IG bit: Group address (multicast/broadcast)
Source: 82:80:58:01:00:01 (82:80:58:01:00:01)
.... ..1. .... .... .... .... = LG bit: Locally administered address (this is NOT the factory default)
.... ...0 .... .... .... .... = IG bit: Individual address (unicast)
Type: IPv4 (0x0800)
[Stream index: 0]
Trailer: 5555555555555555
[Expert Info (Note/Protocol): Didn't find padding of zeros, and an undecoded trailer exists. There may be padding of non-zeros.]
[Didn't find padding of zeros, and an undecoded trailer exists. There may be padding of non-zeros.]
[Severity level: Note]
[Group: Protocol]
Internet Protocol Version 4, Src: 169.254.1.1, Dst: 255.255.255.255
0100 .... = Version: 4
.... 0101 = Header Length: 20 bytes (5)
Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
0000 00.. = Differentiated Services Codepoint: Default (0)
.... ..00 = Explicit Congestion Notification: Not ECN-Capable Transport (0)
Total Length: 38
Identification: 0x01f2 (498)
010. .... = Flags: 0x2, Don't fragment
0... .... = Reserved bit: Not set
.1.. .... = Don't fragment: Set
..0. .... = More fragments: Not set
...0 0000 0000 0000 = Fragment Offset: 0
Time to Live: 128
Protocol: UDP (17)
Header Checksum: 0x4dd6 [validation disabled]
[Header checksum status: Unverified]
Source Address: 169.254.1.1
Destination Address: 255.255.255.255
[Stream index: 0]
User Datagram Protocol, Src Port: 8887, Dst Port: 8888
Source Port: 8887
Destination Port: 8888
Length: 18
Checksum: 0x9fd1 [unverified]
[Checksum Status: Unverified]
[Stream index: 0]
[Stream Packet Number: 4842]
[Timestamps]
[Time since first frame: 1 hour, 29 minutes, 19.050344200 seconds]
[Time since previous frame: 1.024008700 seconds]
UDP payload (10 bytes)
Data (10 bytes)
Data: 434845434b5f4c4f4f50
[Length: 10]

