はじめに
前回の記事では、Topology(Device)をFlowのEndpointとしてTrafficを開始したところ、MAC address resolution failed for IP: 10.1.0.2というエラーが発生した。
当初はARP解決に問題があると考えていたが、切り分けを進めた結果、Flowの作成方法そのものに原因があることが分かった。
本記事では、その切り分けの過程と最終的に動作した最小構成を紹介する。
前回記事はこちら
Port EndpointとDevice Endpointの違い
以前に利用していたPort Endpointと、今回利用するDevice Endpointの違いを図にすると以下のようになる。
【Port Endpoint】
Port
↓
Flow
├ Ethernet
├ IPv4
└ UDP
≒ IxExplorerのStream作成
≒ IxNetworkのL2-3 Quick Flow Group + Packet Editor
【Device Endpoint】
Topology(Device)
└ Ethernet
└ IPv4
↓
Flow
≒ IxNetworkのTopology + Traffic Item
Device EndpointではTopology(Device)配下に構成したInterfaceをFlowのEndpointとして利用する。Packet Headerは省略することもできるが、Flow内でEthernet、IPv4、UDPなどのPacket Headerを定義し、各フィールド値を明示的に設定することも可能である。
Device Endpointを使ったFlow作成の検証
前回はTopology(Device)とIPv4 Interfaceを作成した。
Topology(Device)をFlowのEndpointとして指定し、実際にトラフィックを印加したところ、MAC address resolution failed for IP: 10.1.0.2というエラーが発生した。Port Endpointを使用した前回のサンプルでは、Ethernet/IPv4/UDPヘッダ内で明示的にIP/MACアドレスを指定したTrafficの印加は確認していたため、veth-a/veth-z間でフレームを流すこと自体は可能であると判明していた。
こちらはDevice Endpointを使用して試したが、MACアドレス解決に失敗したコンフィグである。
※現行のサンプルコードをチェックしたところ、protocol_state() / transmit_state() が今の snappiでは互換性は維持するもののcontrol_state() の利用が推奨される方向のようなので設定をいくつか変更をしている
import time
import snappi
def test_topology_ipv4_udp():
# ------------------------------------------------------------------
# Controllerへ接続
# ------------------------------------------------------------------
api = snappi.api(
location="https://localhost:8443",
verify=False
)
# ------------------------------------------------------------------
# Config作成
# ------------------------------------------------------------------
cfg = api.config()
# ------------------------------------------------------------------
# Port作成
# ------------------------------------------------------------------
p1 = cfg.ports.add(name="p1", location="veth-a")
p2 = cfg.ports.add(name="p2", location="veth-z")
# ------------------------------------------------------------------
# Topology(Device)作成
# ------------------------------------------------------------------
topo1 = cfg.devices.add(name="Topology1")
topo2 = cfg.devices.add(name="Topology2")
# ------------------------------------------------------------------
# Topology1にEthernet/IPv4 Interfaceを作成
# ------------------------------------------------------------------
eth1 = topo1.ethernets.add(name="Ethernet1")
eth1.connection.port_name = p1.name
eth1.mac = "00:00:00:00:00:01"
ip1 = eth1.ipv4_addresses.add(name="IPv4-1")
ip1.address = "10.1.0.1"
ip1.gateway = "10.1.0.2"
ip1.prefix = 24
# ------------------------------------------------------------------
# Topology2にEthernet/IPv4 Interfaceを作成
# ------------------------------------------------------------------
eth2 = topo2.ethernets.add(name="Ethernet2")
eth2.connection.port_name = p2.name
eth2.mac = "00:00:00:00:00:02"
ip2 = eth2.ipv4_addresses.add(name="IPv4-2")
ip2.address = "10.1.0.2"
ip2.gateway = "10.1.0.1"
ip2.prefix = 24
# ------------------------------------------------------------------
# Flow1作成
# Device EndpointではIPv4 Interface名を指定
# ------------------------------------------------------------------
flow1 = cfg.flows.add(name="Flow1")
flow1.tx_rx.device.tx_names = [ip1.name]
flow1.tx_rx.device.rx_names = [ip2.name]
flow1.metrics.enable = True
flow1.duration.fixed_packets.packets = 100
flow1.size.fixed = 512
eth_f1, ip_f1, udp1, cus1 = flow1.packet.ethernet().ipv4().udp().custom()
udp1.src_port.value = 2025
udp1.dst_port.value = 2026
cus1.bytes = "".join([hex(c)[2:] for c in b"Topology1"])
# ------------------------------------------------------------------
# Flow2作成
# Device EndpointではIPv4 Interface名を指定
# ------------------------------------------------------------------
flow2 = cfg.flows.add(name="Flow2")
flow2.tx_rx.device.tx_names = [ip2.name]
flow2.tx_rx.device.rx_names = [ip1.name]
flow2.metrics.enable = True
flow2.duration.fixed_packets.packets = 100
flow2.size.fixed = 512
eth_f2, ip_f2, udp2, cus2 = flow2.packet.ethernet().ipv4().udp().custom()
udp2.src_port.value = 2026
udp2.dst_port.value = 2025
cus2.bytes = "".join([hex(c)[2:] for c in b"Topology2"])
# ------------------------------------------------------------------
# Config確認
# ------------------------------------------------------------------
print("\nCONFIGURATION", cfg.serialize(encoding=cfg.JSON), sep="\n")
# ------------------------------------------------------------------
# Config反映
# ------------------------------------------------------------------
api.set_config(cfg)
# ------------------------------------------------------------------
# Protocol起動
# ------------------------------------------------------------------
cs = api.control_state()
cs.protocol.all.state = cs.protocol.all.START
api.set_control_state(cs)
time.sleep(10)
# ------------------------------------------------------------------
# Traffic開始
# ここで MAC address resolution failed が発生
# ------------------------------------------------------------------
cs = api.control_state()
cs.traffic.flow_transmit.state = cs.traffic.flow_transmit.START
api.set_control_state(cs)
失敗ログの抜粋
Exception: code: 500
errors:
- Error occurred while starting flows:
[error starting tx port p1:
unsuccessful Response:
MAC address resolution failed for IP: 10.1.0.2.]
kind: internal
次にtcpdumpを使ってARP解決のやりとりをしているのかを確認しようとveth-a/veth-zおよびtraffic-engine内でtcpdumpを使ってパケットキャプチャを試みたがARP Requestを確認することはできなかった。
veth-a/veth-zのキャプチャ(vethにDevice EndpointのARPが出ていない)
sudo tcpdump -eni veth-a arp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on veth-a, link-type EN10MB (Ethernet), snapshot length 262144 bytes
^C
0 packets captured
0 packets received by filter
0 packets dropped by kernel
sudo tcpdump -eni veth-z arp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on veth-z, link-type EN10MB (Ethernet), snapshot length 262144 bytes
^C
0 packets captured
0 packets received by filter
0 packets dropped by kernel
traffic-engineでのキャプチャ
docker exec -it ixia-c-traffic-engine-veth-z tcpdump -eni veth-a arp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on veth-a, link-type EN10MB (Ethernet), snapshot length 262144 bytes
^C
0 packets captured
0 packets received by filter
0 packets dropped by kernel
docker exec -it ixia-c-traffic-engine-veth-z tcpdump -eni veth-z arp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on veth-z, link-type EN10MB (Ethernet), snapshot length 262144 bytes
^C
0 packets captured
0 packets received by filter
0 packets dropped by kernel
ここまでの切り分けの結果としては以下となる。
Port Endpoint で MAC/IP を明示的に指定した場合は Traffic を流せたが、
Device Endpoint では ARP 解決時に MAC address resolution failed となった。
traffic-engine コンテナ内で veth-a / veth-z を tcpdump しても
Traffic開始前からtcpdumpを実行した状態で確認したが、ARP Requestは一切観測できなかった。
次の切り分けとして、失敗したコードからPacket Headerの定義を除いた最小構成を試みた。
flow.packet.ethernet().ipv4().udp() を入れず、Packet Headerを明示せず、DeviceのIPv4 Interface情報だけで Flow を作る方法を試みている。
# Python標準ライブラリ(試験時間の待機処理に使用)
import time
# OTG APIを操作するPython SDK
import snappi
def test_device_endpoint_minimum():
# ------------------------------------------------------------------
# Controllerへ接続
# ------------------------------------------------------------------
api = snappi.api(
location="https://localhost:8443",
verify=False
)
# ------------------------------------------------------------------
# Config作成
# ------------------------------------------------------------------
cfg = api.config()
# ------------------------------------------------------------------
# Port作成
# ------------------------------------------------------------------
p1 = cfg.ports.add(
name="p1",
location="veth-a"
)
p2 = cfg.ports.add(
name="p2",
location="veth-z"
)
# ------------------------------------------------------------------
# Device作成
# ------------------------------------------------------------------
dev1 = cfg.devices.add(name="Device1")
dev2 = cfg.devices.add(name="Device2")
# ------------------------------------------------------------------
# Device1: Ethernet / IPv4 Interface
# ------------------------------------------------------------------
eth1 = dev1.ethernets.add(name="Device1.Ethernet1")
eth1.connection.port_name = p1.name
eth1.mac = "00:00:00:00:00:01"
ip1 = eth1.ipv4_addresses.add(name="Device1.IPv4")
ip1.address = "10.1.0.1"
ip1.gateway = "10.1.0.2"
ip1.prefix = 24
# ------------------------------------------------------------------
# Device2: Ethernet / IPv4 Interface
# ------------------------------------------------------------------
eth2 = dev2.ethernets.add(name="Device2.Ethernet1")
eth2.connection.port_name = p2.name
eth2.mac = "00:00:00:00:00:02"
ip2 = eth2.ipv4_addresses.add(name="Device2.IPv4")
ip2.address = "10.1.0.2"
ip2.gateway = "10.1.0.1"
ip2.prefix = 24
# ------------------------------------------------------------------
# Flow: Device1 → Device2
# ------------------------------------------------------------------
flow1 = cfg.flows.add(name="Device1_to_Device2")
# Device Endpointを使用
flow1.tx_rx.device.tx_names = [ip1.name]
flow1.tx_rx.device.rx_names = [ip2.name]
# Frame Size
flow1.size.fixed = 512
# Rate
flow1.rate.percentage = 10
# Duration
flow1.duration.fixed_seconds.seconds = 10
# Metrics
flow1.metrics.enable = True
# ------------------------------------------------------------------
# Config確認
# ------------------------------------------------------------------
print("\nCONFIGURATION", cfg.serialize(encoding=cfg.JSON), sep="\n")
# ------------------------------------------------------------------
# Config反映
# ------------------------------------------------------------------
api.set_config(cfg)
# ------------------------------------------------------------------
# Protocol起動
# ------------------------------------------------------------------
state = api.control_state()
state.protocol.all.state = state.protocol.all.START
api.set_control_state(state)
print("Protocol Started")
# ARP解決待ち
time.sleep(10)
# ------------------------------------------------------------------
# Traffic印加開始
# ------------------------------------------------------------------
state = api.control_state()
state.traffic.flow_transmit.state = state.traffic.flow_transmit.START
state.traffic.flow_transmit.flow_names = [flow1.name]
api.set_control_state(state)
print("Traffic Started")
# ------------------------------------------------------------------
# Metrics取得
# ------------------------------------------------------------------
request = api.metrics_request()
request.flow.flow_names = [flow1.name]
for _ in range(20):
metrics = api.get_metrics(request)
print(metrics)
if metrics.flow_metrics[0].transmit == metrics.flow_metrics[0].STOPPED:
break
time.sleep(1)
# ------------------------------------------------------------------
# Traffic印加停止
# ------------------------------------------------------------------
state = api.control_state()
state.traffic.flow_transmit.state = state.traffic.flow_transmit.STOP
state.traffic.flow_transmit.flow_names = [flow1.name]
api.set_control_state(state)
print("Traffic Stopped")
# ------------------------------------------------------------------
# Protocol停止
# ------------------------------------------------------------------
state = api.control_state()
state.protocol.all.state = state.protocol.all.STOP
api.set_control_state(state)
print("Protocol Stopped")
結果はこちら
./do.sh pytest examples/minimum-device-endpoint.py
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-7.1.1, pluggy-1.6.0 -- /home/test/lab/ixia-c/conformance/.env/bin/python
cachedir: .pytest_cache
rootdir: /home/test/lab/ixia-c/conformance, configfile: pytest.ini
plugins: html-4.2.0
collecting ... collected 1 item
examples/minimum-device-endpoint.py::test_device_endpoint_minimum 2026-07-02 14:00:48.685 [snappi] [WARNING] Version check is disabled
-------------------------------- live log call ---------------------------------
WARNING snappi:snappi.py:106 Version check is disabled
2026-07-02 14:00:48.685 [snappi] [WARNING] Certificate verification is disabled
WARNING snappi:snappi.py:164 Certificate verification is disabled
CONFIGURATION
{
"devices": [
{
"ethernets": [
{
"connection": {
"choice": "port_name",
"port_name": "p1"
},
"ipv4_addresses": [
{
"address": "10.1.0.1",
"gateway": "10.1.0.2",
"name": "Device1.IPv4",
"prefix": 24
}
],
"mac": "00:00:00:00:00:01",
"mtu": 1500,
"name": "Device1.Ethernet1"
}
],
"name": "Device1"
},
{
"ethernets": [
{
"connection": {
"choice": "port_name",
"port_name": "p2"
},
"ipv4_addresses": [
{
"address": "10.1.0.2",
"gateway": "10.1.0.1",
"name": "Device2.IPv4",
"prefix": 24
}
],
"mac": "00:00:00:00:00:02",
"mtu": 1500,
"name": "Device2.Ethernet1"
}
],
"name": "Device2"
}
],
"flows": [
{
"duration": {
"choice": "fixed_seconds",
"fixed_seconds": {
"gap": 12,
"seconds": 10
}
},
"metrics": {
"enable": true,
"loss": false,
"timestamps": false
},
"name": "Device1_to_Device2",
"rate": {
"choice": "percentage",
"percentage": 10
},
"size": {
"choice": "fixed",
"fixed": 512
},
"tx_rx": {
"choice": "device",
"device": {
"mode": "mesh",
"rx_names": [
"Device2.IPv4"
],
"tx_names": [
"Device1.IPv4"
]
}
}
}
],
"ports": [
{
"location": "veth-a",
"name": "p1"
},
{
"location": "veth-z",
"name": "p2"
}
]
}
Protocol Started
Traffic Started
choice: flow_metrics
flow_metrics:
- bytes_rx: '249856'
bytes_tx: '304640'
frames_rx: '488'
frames_rx_rate: 0
frames_tx: '595'
frames_tx_rate: 0
name: Device1_to_Device2
rx_rate_bps: 0
transmit: started
tx_rate_bps: 0
--------
choice: flow_metrics
flow_metrics:
- bytes_rx: '1218657792'
bytes_tx: '1218657792'
frames_rx: '2380191'
frames_rx_rate: 237863.45
frames_tx: '2380191'
frames_tx_rate: 237859.34
name: Device1_to_Device2
rx_rate_bps: 974288700
transmit: stopped
tx_rate_bps: 974271900
Traffic Stopped
Protocol Stopped
PASSED
このコードを使ったところ試験は通った。MetricsでもTx/Rx が試験停止後の最終値での一致を確認した(以下抜粋)。
frames_tx: 2380191
frames_rx: 2380191
transmit: stopped
切り分け等による知見
失敗したコードではPacket Header部分を自分で定義していた。
flow1.packet.ethernet()
flow1.packet.ipv4()
flow1.packet.udp()
今回の切り分けでは、Packet Headerを定義しない最小構成でTrafficを印加できることを確認した。
flow1.tx_rx.device.tx_names = [ip1.name]
flow1.tx_rx.device.rx_names = [ip2.name]
その後さらに検証を進めた結果、Packet Headerを追加し、MACアドレス・IPアドレス・UDP Portを適切に設定したところ、正常にTrafficを印加できることが分かった。
Packet Headerを定義しない最小構成では何が送信されるのか
最小構成でTrafficは印加できたため、実際にどのようなフレームが送信されているのかを確認する目的でキャプチャしたパケットをWiresharkで確認した。
0xffffでIPv4 0x0800でもない。Device1 / Device2 に設定した 10.1.0.1, 10.1.0.2, 00:00:00:00:00:01, 00:00:00:00:00:02 が、Flowの実パケットヘッダに入っていない状態であることが見て取れた。
今回確認した最小構成では、flow1.tx_rx.device.tx_names / rx_names はFlowの送信・受信Endpointを指定するものであり、Deviceに設定したMACアドレスやIPアドレスがFlowのPacket Headerへ自動反映される動作は確認できなかった。
$ sudo timeout 15 tcpdump -i veth-z -w device_endpoint.pcap
tcpdump: listening on veth-z, link-type EN10MB (Ethernet), snapshot length 262144 bytes
658770 packets captured
658971 packets received by filter
※今回使っている設定は片方向のFlow(Device1→Device2)への印加の設定しか使ってないため、Device2(veth-z)のキャプチャを実施している。
Flowの設定でPacket Headerへ各フィールド値を設定する
IPアドレス/MACアドレス/Port番号を入れる場合の設定例は以下になる。
# ------------------------------------------------------------------
# 今までのFlow設定
# ------------------------------------------------------------------
flow1.tx_rx.device.tx_names = [ip1.name]
flow1.tx_rx.device.rx_names = [ip2.name]
# ------------------------------------------------------------------
# 追加箇所
# ------------------------------------------------------------------
eth, ipv4, udp = flow1.packet.ethernet().ipv4().udp()
eth.src.value = "00:00:00:00:00:01"
eth.dst.value = "00:00:00:00:00:02"
ipv4.src.value = "10.1.0.1"
ipv4.dst.value = "10.1.0.2"
udp.src_port.value = 50000
udp.dst_port.value = 50001
Wiresharkで再度確認。IPアドレス/MACアドレス 任意に設定したUDPポート番号が付与されている。
当初は、Device EndpointではPacket Headerを定義せず、Device配下のIPv4 InterfaceからPacket Headerが自動生成されるものと考えていた。しかし実際に検証を進めたところ、その理解は正確ではなかった。
Device EndpointでもFlow内でPacket Headerを定義でき、MACアドレスやIPアドレスなどを適切に設定することで正常にTrafficを印加できることを確認した。
まとめ
今回の切り分けで分かったことは以下である。
Port Endpointでは、Flow内でPacket Header(Ethernet/IPv4/UDPなど)を明示的に定義する。
一方、Device Endpointでは、Device配下のIPv4 InterfaceをEndpointとして指定してTrafficを生成する。Device EndpointはFlowの送受信Endpointを指定するための設定であり、Packet Headerを自動生成する機能とは別であることが分かった。
また、Packet Headerを省略した最小構成でもTrafficを印加できることを確認した。一方で、MACアドレスやIPアドレス、UDP Portを利用した通信を行う場合は、Flow内でPacket Headerを定義し、各フィールドを明示的に設定する必要があることも確認できた。

