はじめに
「メリーーーークリスマス!!!!」 ( 渋谷より 哀 を込めて )
この記事がアドベントカレンダーで公開される頃には、渋谷で一人、お酒を飲んでいることだろう。こんな記事書いてないで with でも始めろと思われるでしょうが、私みたいに一人寂しくしている NW エンジニアの方もいることだろうと思い、今回この記事を書いています。

(↑あくまでイメージ!!、本物は、もっと、こう、なんだろう?、、、いい感じです...よ)
...さて、本題へ移ろうか。
今回は、タイトル通り Simple Two-Way Active Measurement Protocol (STAMP: RFC8762) について軽く説明します。
前提として、ネットワークの品質、特に「遅延(レイテンシ)」を測定する手段として、最も広く知られている手法は Ping です。手軽に疎通確認と往復時間(RTT)が測れるため、NW エンジニアにとって必須のツールであると言えます。
しかし、近年求められる高品質なネットワークにおいては、Ping だけでは不十分なケースが増えています。高精度な測定手法として、UDP パケットを使用する以下のプロトコルが標準化されています。
-
OWAMP (RFC 4656): 片方向遅延 (One-Way Active Measurement Protocol)
-
TWAMP (RFC 5357): 双方向遅延 (Two-Way Active Measurement Protocol)
特に TWAMP は、双方向で遅延を測定するためのプロトコルとして有名です。
TWAMP は TWAMP-Control と TWAMP-Test という2つのコンポーネントで構成されています。測定を行うには、事前に TWAMP-Control によるセッションの確立や管理といったやり取りが必要であり、仕組みが複雑でした。
そこで、この手順を簡略化した TWAMP Light が考案されました。TWAMP Light は、TWAMP-Control によるネゴシエーションを省略し、仕組みを単純化しています。
しかし、TWAMP Light は RFC 5357 の付録で言及されている程度であり、定義が曖昧でした。その結果、ベンダーごとの解釈や実装に差異が生じ、相互接続性が保たれないケースが発生します。
こうした課題を解決するために、テストパケットの送受信機能に特化して標準化されたプロトコルが STAMP になります。
STAMPとは
以下で軽く STAMP について触れていきます。
パケット構造
STAMP は Sender 送信する要求パケットと、Reflector が送り返す返信パケットの2種類を用いて双方向の遅延測定を行います。様々なフィールドがありますが、大事なのが Timestamp であり、この Timestamp フィールドを用いて遅延を測定します。(参考程度に以下に図を貼っています)
動作の流れ
STAMPの動作フローを順を追って説明します。 基本的には Session-Sender と、パケットを折り返す Session-Reflector の間でパケットを一往復させます。
まず、正確な片方向遅延を測定するためには、Sender と Reflector の間で NTP や PTP を用いた時刻同期を行います。ここがずれていると、往復遅延 (RTT) は測れても、片道遅延の計算結果にズレが生じます。
測定が開始されると、Sender は要求パケットを生成します。このとき、パケットを送信した瞬間の時刻 $T_1$ をパケット内部に記録し、Reflector へ向けて送信します。
Reflector は要求パケットを受け取ると、その瞬間の受信時刻 $T_2$ を記録します。
Reflectorは、受信した要求パケットに対して返信処理を行います。受信パケットの中身($T_1$)をコピーし、そこに受信時刻($T_2$)と、送り返そうとしている送信時刻 $T_3$ を返信パケットに書き込んで、即座にSenderへ送り返します。
最後に、Sender が返信パケットを受け取り、その受信時刻 $T_4$ を記録します。この時点で、Senderの手元には ${T_1, T_2, T_3, T_4}$ という4つのタイムスタンプが揃います。これらを使って、行きと帰りの遅延を個別に計算します。
上り方向遅延 (Sender→Reflector):$$\delta_f = T_2 - T_1$$下り方向遅延 (Reflector→Sender):$$\delta_r = T_4 - T_3$$これにより、ネットワークの混雑状況などで変わりやすい「行き」と「帰り」の遅延をそれぞれ把握することが可能になります。
試作
簡単なコードを gemini に作ってもらいました。(便利ですねぇ)
共通ヘッダ
#ifndef STAMP_H
#define STAMP_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#define NTP_OFFSET 2208988800UL
struct stamp_packet {
uint32_t seq_num;
uint32_t timestamp_sec;
uint32_t timestamp_frac;
uint16_t error_estimate;
uint16_t ssid;
uint32_t rx_sec;
uint32_t rx_frac;
uint32_t tx_sec;
uint32_t tx_frac;
} __attribute__((packed));
void get_ntp_timestamp(uint32_t *sec, uint32_t *frac) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
*sec = htonl((uint32_t)(ts.tv_sec + NTP_OFFSET));
double fraction = (double)ts.tv_nsec * (double)(1LL << 32) / 1000000000.0;
*frac = htonl((uint32_t)fraction);
}
double ntp_to_double(uint32_t sec, uint32_t frac) {
uint32_t s = ntohl(sec);
uint32_t f = ntohl(frac);
return (double)(s - NTP_OFFSET) + ((double)f / (double)(1LL << 32));
}
#endif
STAMP Sender
#include "stamp.h"
#define PORT 8762
#define SERVER_IP "127.0.0.1"
int main(int argc, char *argv[]) {
int sockfd;
struct sockaddr_in servaddr;
struct stamp_packet tx_packet, rx_packet;
socklen_t len;
const char* ip = (argc > 1) ? argv[1] : SERVER_IP;
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
if (inet_pton(AF_INET, ip, &servaddr.sin_addr) <= 0) {
perror("Invalid address");
return -1;
}
printf("STAMP Sender targeting %s:%d\n", ip, PORT);
printf("Seq\tForward(ms)\tBackward(ms)\tRTT(ms)\n");
printf("----------------------------------------------------\n");
uint32_t seq = 0;
while (1) {
memset(&tx_packet, 0, sizeof(tx_packet));
tx_packet.seq_num = htonl(seq++);
tx_packet.ssid = htons(1);
uint32_t t1_sec, t1_frac;
get_ntp_timestamp(&t1_sec, &t1_frac);
tx_packet.timestamp_sec = t1_sec;
tx_packet.timestamp_frac = t1_frac;
sendto(sockfd, &tx_packet, sizeof(tx_packet), 0, (const struct sockaddr *)&servaddr, sizeof(servaddr));
len = sizeof(servaddr);
int n = recvfrom(sockfd, &rx_packet, sizeof(rx_packet), 0, (struct sockaddr *)&servaddr, &len);
uint32_t t4_sec, t4_frac;
get_ntp_timestamp(&t4_sec, &t4_frac);
if (n > 0) {
double t1 = ntp_to_double(rx_packet.timestamp_sec, rx_packet.timestamp_frac);
double t2 = ntp_to_double(rx_packet.rx_sec, rx_packet.rx_frac);
double t3 = ntp_to_double(rx_packet.tx_sec, rx_packet.tx_frac);
double t4 = ntp_to_double(t4_sec, t4_frac);
double forward_delay = (t2 - t1) * 1000.0;
double backward_delay = (t4 - t3) * 1000.0;
double rtt = (t4 - t1) * 1000.0 - (t3 - t2) * 1000.0;
printf("%u\t%.3f\t\t%.3f\t\t%.3f\n", ntohl(rx_packet.seq_num), forward_delay, backward_delay, rtt);
}
sleep(1);
}
close(sockfd);
return 0;
}
STAMP Reflector
#include "stamp.h"
#define PORT 8762
int main() {
int sockfd;
struct sockaddr_in servaddr, cliaddr;
struct stamp_packet packet;
socklen_t len;
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("STAMP Reflector listening on port %d...\n", PORT);
while (1) {
len = sizeof(cliaddr);
int n = recvfrom(sockfd, &packet, sizeof(packet), 0, (struct sockaddr *)&cliaddr, &len);
uint32_t t2_sec, t2_frac;
get_ntp_timestamp(&t2_sec, &t2_frac);
if (n > 0) {
packet.rx_sec = t2_sec;
packet.rx_frac = t2_frac;
uint32_t t3_sec, t3_frac;
get_ntp_timestamp(&t3_sec, &t3_frac);
packet.tx_sec = t3_sec;
packet.tx_frac = t3_frac;
sendto(sockfd, &packet, sizeof(packet), 0, (struct sockaddr *)&cliaddr, len);
printf("Reflected packet Seq: %u\n", ntohl(packet.seq_num));
}
}
close(sockfd);
return 0;
}
実行結果
簡単にテストしたかったので、CML 上で以下の構成を作成し、軽く動かしてみました。sender はそのままSTAMP_Senderを意味し、reflector はSTAMP_Reflector を意味しています。中央にある ubuntu は遅延を挿入するために用意しています。また、時刻同期には chorny を使用しています。

(※ ext-conn-0 と unmanaged-switch-0 は気にしないでくださいmm、サーバがインターネットに出るためです)
結果: (遅延なし)
| Seq | Forward(ms) | Backward(ms) | RTT(ms) |
|---|---|---|---|
| 0 | 2.589 | 1.649 | 4.238 |
| 1 | 2.622 | 1.915 | 4.538 |
| 2 | 2.934 | 1.583 | 4.517 |
| 3 | 2.743 | 1.974 | 4.717 |
| 4 | 2.972 | 1.497 | 4.469 |
| 5 | 2.440 | 1.330 | 3.771 |
| 6 | 2.687 | 1.912 | 4.598 |
| 7 | 2.735 | 1.386 | 4.122 |
| 8 | 2.958 | 1.538 | 4.496 |
| 9 | 2.799 | 1.756 | 4.555 |
| 10 | 2.725 | 1.729 | 4.454 |
| 11 | 2.555 | 1.534 | 4.089 |
| 12 | 2.757 | 1.374 | 4.132 |
| 13 | 2.950 | 1.454 | 4.404 |
| 14 | 2.926 | 1.730 | 4.656 |
| 15 | 2.486 | 1.574 | 4.060 |
| 16 | 2.592 | 1.532 | 4.125 |
| 17 | 2.667 | 1.885 | 4.552 |
| 18 | 2.360 | 1.548 | 3.908 |
| 19 | 3.034 | 1.824 | 4.858 |
| 20 | 2.618 | 1.586 | 4.204 |
| 21 | 2.319 | 1.691 | 4.011 |
若干、Forwardの方が高いことがわかります。
結果: (sender => refector 上り方向の通信に +5 ms)
ubuntu で以下のtcコマンドを実行して +5ms の遅延を付与しています。
sudo tc qdisc add dev ens3 root netem delay 5ms
| Seq | Forward(ms) | Backward(ms) | RTT(ms) |
|---|---|---|---|
| 0 | 8.007 | 1.617 | 9.624 |
| 1 | 7.846 | 1.594 | 9.440 |
| 2 | 7.664 | 1.755 | 9.420 |
| 3 | 7.885 | 1.697 | 9.582 |
| 4 | 7.906 | 1.666 | 9.571 |
| 5 | 7.909 | 1.415 | 9.325 |
| 6 | 8.011 | 1.686 | 9.697 |
| 7 | 7.755 | 1.868 | 9.623 |
| 8 | 7.981 | 1.775 | 9.756 |
| 9 | 8.119 | 1.796 | 9.916 |
| 10 | 8.053 | 1.983 | 10.037 |
| 11 | 7.922 | 1.587 | 9.509 |
| 12 | 7.855 | 2.167 | 10.022 |
| 13 | 8.081 | 1.666 | 9.747 |
| 14 | 9.071 | 1.511 | 10.583 |
| 15 | 8.066 | 1.516 | 9.582 |
| 16 | 8.045 | 1.633 | 9.678 |
| 17 | 8.081 | 1.884 | 9.965 |
| 18 | 8.226 | 1.748 | 9.974 |
| 19 | 8.001 | 1.697 | 9.698 |
| 20 | 7.755 | 1.662 | 9.418 |
| 21 | 7.858 | 1.609 | 9.467 |
Forwardを確認すると、Backwardと比較して、遅延が大きいことがわかり、片方向のみの遅延増加が数値として明確に表れています。 Backwardの数値は維持されていることから、STAMPはネットワークの非対称な遅延状況を正しく反映しており、RTT計測だけでは判別できない方向ごとの通信品質を測定できていると言えます。
さいごに
今回は、STAMP についてご説明させていただきました〜。
皆様、ありがとうございました!!








