2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クリスマスの夜に「RFC8762: 双方向遅延測定プロトコル STAMP」について勉強しよう!!

Last updated at Posted at 2025-12-22

はじめに

「メリーーーークリスマス!!!!」  ( 渋谷より 哀 を込めて )

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

Gemini_Generated_Image_p53x69p53x69p53x.png
(↑あくまでイメージ!!、本物は、もっと、こう、なんだろう?、、、いい感じです...よ)

...さて、本題へ移ろうか。

今回は、タイトル通り 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 フィールドを用いて遅延を測定します。(参考程度に以下に図を貼っています)

スクリーンショット 2025-12-22 22.33.27.png
( ↑ 要求パケットの構造 )

スクリーンショット 2025-12-22 22.33.46.png
( ↑ 返信パケットの構造 )

動作の流れ

 STAMPの動作フローを順を追って説明します。 基本的には Session-Sender と、パケットを折り返す Session-Reflector の間でパケットを一往復させます。

 まず、正確な片方向遅延を測定するためには、Sender と Reflector の間で NTP や PTP を用いた時刻同期を行います。ここがずれていると、往復遅延 (RTT) は測れても、片道遅延の計算結果にズレが生じます。

スクリーンショット 2025-12-21 21.33.33.png

 測定が開始されると、Sender は要求パケットを生成します。このとき、パケットを送信した瞬間の時刻 $T_1$ をパケット内部に記録し、Reflector へ向けて送信します。

スクリーンショット 2025-12-21 21.33.46.png

 Reflector は要求パケットを受け取ると、その瞬間の受信時刻 $T_2$ を記録します。

スクリーンショット 2025-12-21 21.33.57.png

 Reflectorは、受信した要求パケットに対して返信処理を行います。受信パケットの中身($T_1$)をコピーし、そこに受信時刻($T_2$)と、送り返そうとしている送信時刻 $T_3$ を返信パケットに書き込んで、即座にSenderへ送り返します。

スクリーンショット 2025-12-21 21.34.05.png

 最後に、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$$これにより、ネットワークの混雑状況などで変わりやすい「行き」と「帰り」の遅延をそれぞれ把握することが可能になります。

スクリーンショット 2025-12-21 21.34.54.png

試作

簡単なコードを gemini に作ってもらいました。(便利ですねぇ)

共通ヘッダ
stamp.h
#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
sender.c
#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
reflector.c
#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 を使用しています。

スクリーンショット 2025-12-23 0.36.51.png
(※ ext-conn-0 と unmanaged-switch-0 は気にしないでくださいmm、サーバがインターネットに出るためです)

結果: (遅延なし)

スクリーンショット 2025-12-23 0.43.21.png

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

スクリーンショット 2025-12-23 0.49.50.png

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 についてご説明させていただきました〜。
皆様、ありがとうございました!!

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?