1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【eBPF】TCP通信をフックして通信量を計測してみた

Posted at

eBPFプログラムの実装について学習した内容をアウトプットする記事となります。
私はC言語、eBPFプログラムを初めて触る初心者のため、記事の内容も初学者向けのものとなっています。

eBPFの概要は公式ドキュメントを参照して下さい。

目次

eBPFプログラムの概要

スクリーンショット 2025-08-17 22.56.26.png

GoogleCloudのGCEインスタンスで、カーネルのtcp_sendmsg, tcp_cleanup_rbuf関数をフックするeBPFプログラムを実装します。
eBPFプログラム(agent.o)は、送信元・宛先のIPアドレスの組み合わせ毎に通信量(TX, RX)を加算して、eBPFマップに保存します。
ユーザー空間プログラム(agent_checker)は定期的にeBPFマップを読み込み、内容を標準出力します。

環境情報

以下、実行環境のサーバー情報です。

コンポーネント バージョン
OS Ubuntu 24.04.3 LTS
Kernel 6.14.0-1012-gcp
Clang 17.0.6
bpftool v7.6.0
libbpf v1.6

環境構築

TerraformでGCEインスタンスを構築します。

main.tf
variable "GC_CREDENTIALS_PATH" {
  type        = string
  description = "Path for GoogleCloud credentials."
}

variable "GC_PROJECT" {
  type        = string
  description = "GoogleCloud project name."
}

locals {
  project = var.GC_PROJECT
  region  = "asia-northeast1"
  zone    = "asia-northeast1-c"
  servername = "dev-server"
}

terraform {
  required_version = "~> 1.14.0"

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 6.48"
    }
  }

  backend "gcs" {
    bucket = "tmatsuno-gke-test-tfstate"
    prefix = "ebpf-hook-tcp-test"
  }
}

provider "google" {
  credentials = file(var.GC_CREDENTIALS_PATH)

  project = local.project
  region  = local.region
  zone    = local.zone
}

output "dev_server_ssh_command" {
  value = "gcloud compute ssh <SSHユーザー名>@${google_compute_instance.dev_server.name} --project=${local.project} --zone=${local.zone}"
}

resource "google_compute_instance" "dev_server" {
  name         = local.servername
  machine_type = "e2-small"

  boot_disk {
    auto_delete = true
    source      = google_compute_disk.default.self_link
  }

  network_interface {
    network = "default"
    access_config {}
  }

  metadata_startup_script = <<EOF
#!/bin/bash
sudo su
apt-get update
apt-get install -y apt-transport-https gnupg wget
wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add -
echo "deb http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-17 main" > /etc/apt/sources.list.d/llvm.list

apt-get update
apt-get -y install \
  git \
  pkg-config \
  iproute2 \
  strace \
  build-essential \
  gdb \
  clang-17 \
  lld-17 \
  llvm \
  libelf-dev \
  zlib1g-dev \
  libbpf-dev \
  linux-headers-$(uname -r)
apt-get -y upgrade libbpf-dev

# bpftrace, bpfcc を利用したい場合はコメントイン
# apt-get -y install bpftrace bpfcc-tools libbpfcc libbpfcc-dev

ln -s /usr/bin/clang-17 /usr/bin/clang

# eBPFプログラムがカーネルのトレースポイントを利用できるようにするために設定
echo "kernel.perf_event_paranoid = 1" | tee -a /etc/sysctl.conf > /dev/null
sysctl -p

git clone https://github.com/libbpf/libbpf.git  /tmp/libbpf
git clone --recurse-submodules https://github.com/libbpf/bpftool.git /tmp/bpftool

cd /tmp/bpftool/src
make install

cd /tmp/libbpf/src
make install

EOF
}

resource "google_compute_disk" "default" {
  name  = "${local.servername}-disk"
  image = "ubuntu-os-cloud/ubuntu-2404-lts-amd64"
  size  = 20
}

eBPFプログラムの実装

以下のファイルを作成します。

.
├── Makefile          # Cプログラムをコンパイルするための設定ファイル
├── bpf_common.h      # 共通利用する型定義ファイル
├── agent.c           # TCP通信のbyte数合計をeBPFマップに書き込む、eBPFプログラム
└── agent_checker.c   # 定期的にeBPFマップを読み込み標準出力する、ユーザー空間プログラム

bpf_common.h

eBPFマップのキー・バリューとして使用する構造体を定義します。

IPアドレスはIPv4-mapped (IPv6) addressという形式で配列に格納します。
カーネルは内部的に、IPv4ソケットの通信もIPv6ソケットの通信も統一的に扱えるよう、IPv4アドレスを::ffff:192.0.2.1のような形式にマッピングして保持することがあります。

#ifndef BPF_COMMON_H
#define BPF_COMMON_H

// TCP通信を一意に識別するために、送信元(source)と宛先(destination)のIPアドレスを保持して、この組み合わせをキー値とします
struct ip_key_t {
    // IPアドレスを格納する配列
    // 16バイト(128ビット)のサイズを確保することで、IPv4とIPv6の両方に対応できます
    __u8 saddr[16]; // 送信元IPアドレス (source address)
    __u8 daddr[16]; // 宛先IPアドレス (destination address)
};

// eBPFマップのバリュー(値)として使用する構造体。送信または受信した合計バイト数を格納します
struct val_t {
    __u64 bytes;
};

#endif // BPF_COMMON_H

agent.c

カーネル内で発生するTCP通信を捕捉し、送信元・宛先IPアドレスに関連付けてネットワークの送受信バイト数を計測するプログラムを実装します。

eBPFマップの定義

IPアドレスをキーにして、送受信バイト数を格納するマップを2つ作成します。

// 送信(TX: Transmit)バイト数をIPアドレスペアごとに格納するためのeBPFハッシュマップ
struct {
    __uint(type, BPF_MAP_TYPE_HASH); // マップの種類。ハッシュマップは高速な検索が可能です
    __uint(max_entries, 10240);      // マップが保持できる最大のキー・バリューペアの数
    __type(key, struct ip_key_t);    // キーの型を指定
    __type(value, struct val_t);     // バリューの型を指定
} tx_stats SEC(".maps");

// 受信(RX: Receive)バイト数をIPアドレスペアごとに格納するためのeBPFハッシュマップ
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, struct ip_key_t);
    __type(value, struct val_t);
} rx_stats SEC(".maps");

tcp_sendmsg をフックする関数の定義

tcp_sendmsg は、TCPソケットを通じてデータを送信する際に呼ばれる関数です。tcp_sendmsg 関数をフックして、外部に送信されるデータ量(TX)を計測します。

SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(trace_tcp_sendmsg, struct sock *sk, struct msghdr *msg, size_t size) {
    // 送信サイズが0の場合は、統計に影響しないため、ここで処理を終了します
    if (size == 0) {
        return 0;
    }

    struct ip_key_t key = {};
    struct val_t *valp, zero = {};

    // sk (struct sock) は、カーネルがTCP接続などのソケット情報を管理するための構造体です
    // bpf_probe_read_kernelヘルパー関数を使い、カーネルメモリからIPアドレスを読み出します
    bpf_probe_read_kernel(&key.saddr, sizeof(key.saddr), &sk->__sk_common.skc_v6_rcv_saddr);
    bpf_probe_read_kernel(&key.daddr, sizeof(key.daddr), &sk->__sk_common.skc_v6_daddr);

    // IPアドレスペアをキーとして、送信統計マップ(tx_stats)を検索します
    valp = bpf_map_lookup_elem(&tx_stats, &key);
    if (!valp) {
        // マップにこのIPペアのエントリがまだ存在しない場合、新しいエントリをゼロ値で初期化してマップに追加します
        bpf_map_update_elem(&tx_stats, &key, &zero, BPF_NOEXIST);
        valp = bpf_map_lookup_elem(&tx_stats, &key);
        if (!valp) {
            return 0; // 再度検索しても見つからない場合は、エラーとして処理を中断します
        }
    }

    // 取得した送信バイト数(size)をマップの値に加算します
    __sync_fetch_and_add(&valp->bytes, size);

    // 常に0を返して、カーネルの元の処理を妨げないようにします
    return 0;
}

tcp_cleanup_rbuf をフックする関数の定義

tcp_cleanup_rbuf は、TCPの受信バッファからデータが読み出され、受信が完了した後に呼び出される関数です。tcp_cleanup_rbuf 関数をフックして、受信したデータ量(RX)を計測します。

SEC("kprobe/tcp_cleanup_rbuf")
int BPF_KPROBE(trace_tcp_cleanup_rbuf, struct sock *sk, int copied) {
    // コピーされたバイト数が0以下の場合は、統計に影響しないため無視します
    if (copied <= 0) {
        return 0;
    }

    struct ip_key_t key = {};
    struct val_t *valp, zero = {};

    // 送信時とは逆に、受信時は daddr が自分、saddr が相手のアドレスになる
    bpf_probe_read_kernel(&key.saddr, sizeof(key.saddr), &sk->__sk_common.skc_v6_daddr);
    bpf_probe_read_kernel(&key.daddr, sizeof(key.daddr), &sk->__sk_common.skc_v6_rcv_saddr);

    // IPアドレスペアをキーとして、受信統計マップ(rx_stats)を検索・更新します
    valp = bpf_map_lookup_elem(&rx_stats, &key);
    if (!valp) {
        bpf_map_update_elem(&rx_stats, &key, &zero, BPF_NOEXIST);
        valp = bpf_map_lookup_elem(&rx_stats, &key);
        if (!valp) {
            return 0;
        }
    }

    // 受信バイト数(copied)を加算します
    __sync_fetch_and_add(&valp->bytes, copied);

    return 0;
}

全体のコードは以下となります。

agent.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "bpf_common.h"

// 送信(TX: Transmit)バイト数をIPアドレスペアごとに格納するためのeBPFハッシュマップ
struct {
    __uint(type, BPF_MAP_TYPE_HASH); // マップの種類。ハッシュマップは高速な検索が可能です
    __uint(max_entries, 10240);      // マップが保持できる最大のキー・バリューペアの数
    __type(key, struct ip_key_t);    // キーの型を指定
    __type(value, struct val_t);     // バリューの型を指定
} tx_stats SEC(".maps");

// 受信(RX: Receive)バイト数をIPアドレスペアごとに格納するためのeBPFハッシュマップ
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 10240);
    __type(key, struct ip_key_t);
    __type(value, struct val_t);
} rx_stats SEC(".maps");

SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(trace_tcp_sendmsg, struct sock *sk, struct msghdr *msg, size_t size) {
    // 送信サイズが0の場合は、統計に影響しないため、ここで処理を終了します
    if (size == 0) {
        return 0;
    }

    struct ip_key_t key = {};
    struct val_t *valp, zero = {};

    // sk (struct sock) は、カーネルがTCP接続などのソケット情報を管理するための構造体です
    // bpf_probe_read_kernelヘルパー関数を使い、カーネルメモリからIPアドレスを読み出します
    bpf_probe_read_kernel(&key.saddr, sizeof(key.saddr), &sk->__sk_common.skc_v6_rcv_saddr);
    bpf_probe_read_kernel(&key.daddr, sizeof(key.daddr), &sk->__sk_common.skc_v6_daddr);

    // IPアドレスペアをキーとして、送信統計マップ(tx_stats)を検索します
    valp = bpf_map_lookup_elem(&tx_stats, &key);
    if (!valp) {
        // マップにこのIPペアのエントリがまだ存在しない場合、新しいエントリをゼロ値で初期化してマップに追加します
        bpf_map_update_elem(&tx_stats, &key, &zero, BPF_NOEXIST);
        valp = bpf_map_lookup_elem(&tx_stats, &key);
        if (!valp) {
            return 0; // 再度検索しても見つからない場合は、エラーとして処理を中断します
        }
    }

    // 取得した送信バイト数(size)をマップの値に加算します
    __sync_fetch_and_add(&valp->bytes, size);

    // 常に0を返して、カーネルの元の処理を妨げないようにします
    return 0;
}

SEC("kprobe/tcp_cleanup_rbuf")
int BPF_KPROBE(trace_tcp_cleanup_rbuf, struct sock *sk, int copied) {
    // コピーされたバイト数が0以下の場合は、統計に影響しないため無視します
    if (copied <= 0) {
        return 0;
    }

    struct ip_key_t key = {};
    struct val_t *valp, zero = {};

    // 送信時とは逆に、受信時は daddr が自分、saddr が相手のアドレスになります
    bpf_probe_read_kernel(&key.saddr, sizeof(key.saddr), &sk->__sk_common.skc_v6_daddr);
    bpf_probe_read_kernel(&key.daddr, sizeof(key.daddr), &sk->__sk_common.skc_v6_rcv_saddr);

    // IPアドレスペアをキーとして、受信統計マップ(rx_stats)を検索・更新します
    valp = bpf_map_lookup_elem(&rx_stats, &key);
    if (!valp) {
        bpf_map_update_elem(&rx_stats, &key, &zero, BPF_NOEXIST);
        valp = bpf_map_lookup_elem(&rx_stats, &key);
        if (!valp) {
            return 0;
        }
    }

    // 受信バイト数(copied)を加算します
    __sync_fetch_and_add(&valp->bytes, copied);

    return 0;
}

// Linuxカーネル全体は GPL (GNU General Public License) というライセンスで公開されているため、
// eBPFプログラムがカーネルのヘルパー関数などを利用する場合に、GPLライセンスであることが要求されます
char LICENSE[] SEC("license") = "GPL";

agent_checker.c

eBPFマップの定期的に読み込み、内容を標準出力するユーザー空間プログラムを実装します。詳細はコメントを参考にして下さい。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <arpa/inet.h>
#include <bpf/libbpf.h>
#include "bpf_common.h"

static volatile bool exiting = false;
static struct bpf_object *bpf_obj = NULL;

// Ctrl+Cが押されたときに呼ばれるシグナルハンドラ
static void sig_handler(int sig) {
    exiting = true;
}

// IPアドレスのバイト配列を人間が読める文字列に変換するヘルパー関数
static const char *addr_to_str(const __u8 addr[16]) {
    static char str[INET6_ADDRSTRLEN];
    // IPv4射影アドレスのプレフィックス (最初の12バイト)
    static const __u8 v4_mapped_prefix[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff};

    // 最初の12バイトがプレフィックスと一致するかどうかを比較
    if (memcmp(addr, v4_mapped_prefix, sizeof(v4_mapped_prefix)) == 0) {
        // 一致すれば、IPv4アドレスとしてフォーマット
        inet_ntop(AF_INET, addr + 12, str, sizeof(str));
    } else {
        // 一致しなければ、IPv6アドレスとしてフォーマット
        inet_ntop(AF_INET6, addr, str, sizeof(str));
    }
    return str;
}

// eBPFマップの内容を読み出して表示する関数
static void print_stats(struct bpf_map *tx_map, struct bpf_map *rx_map) {
    struct ip_key_t key = {}, prev_key = {};
    struct val_t val;
    char saddr_str[INET6_ADDRSTRLEN];
    char daddr_str[INET6_ADDRSTRLEN];

    printf("\n--- TX Stats ---\n");
    memset(&prev_key, 0, sizeof(prev_key));
    // bpf_map__get_next_keyを使ってマップの全エントリをイテレート
    while (bpf_map__get_next_key(tx_map, &prev_key, &key, sizeof(key)) == 0) {
        if (bpf_map__lookup_elem(tx_map, &key, sizeof(key), &val, sizeof(val), 0) == 0) {
            strcpy(saddr_str, addr_to_str(key.saddr));
            strcpy(daddr_str, addr_to_str(key.daddr));
            printf("  %s -> %s : %llu bytes\n",
                   saddr_str, daddr_str, val.bytes);
        }
        memcpy(&prev_key, &key, sizeof(key));
    }

    printf("--- RX Stats ---\n");
    memset(&prev_key, 0, sizeof(prev_key)); // prev_keyをリセット
    while (bpf_map__get_next_key(rx_map, &prev_key, &key, sizeof(key)) == 0) {
        if (bpf_map__lookup_elem(rx_map, &key, sizeof(key), &val, sizeof(val), 0) == 0) {
            strcpy(saddr_str, addr_to_str(key.saddr));
            strcpy(daddr_str, addr_to_str(key.daddr));
            printf("  %s -> %s : %llu bytes\n",
                   saddr_str, daddr_str, val.bytes);
        }
        memcpy(&prev_key, &key, sizeof(key));
    }
}

// エントリーポイントとなるmain関数
int main(int argc, char **argv) {
    struct bpf_link *links[2] = {NULL, NULL};
    struct bpf_map *tx_map = NULL;
    struct bpf_map *rx_map = NULL;

    signal(SIGINT, sig_handler);
    signal(SIGTERM, sig_handler);

    // 1. eBPFオブジェクトファイルをロード
    bpf_obj = bpf_object__open_file("agent.o", NULL);
    if (libbpf_get_error(bpf_obj)) {
        fprintf(stderr, "ERROR: opening BPF object file failed\n");
        return 1;
    }

    // 2. eBPFプログラムをカーネルにロード
    if (bpf_object__load(bpf_obj)) {
        fprintf(stderr, "ERROR: loading BPF object file failed\n");
        goto cleanup;
    }

    // 3. kprobeをアタッチ
    links[0] = bpf_program__attach(bpf_object__find_program_by_name(bpf_obj, "trace_tcp_sendmsg"));
    if (libbpf_get_error(links[0])) {
        fprintf(stderr, "ERROR: attaching trace_tcp_sendmsg kprobe failed\n");
        goto cleanup;
    }
    links[1] = bpf_program__attach(bpf_object__find_program_by_name(bpf_obj, "trace_tcp_cleanup_rbuf"));
    if (libbpf_get_error(links[1])) {
        fprintf(stderr, "ERROR: attaching trace_tcp_cleanup_rbuf kprobe failed\n");
        goto cleanup;
    }

    printf("Successfully loaded and attached BPF programs. Watching TCP traffic...\n");
    printf("Press Ctrl+C to exit.\n");

    // 4. eBPFマップへのポインタを取得
    tx_map = bpf_object__find_map_by_name(bpf_obj, "tx_stats");
    rx_map = bpf_object__find_map_by_name(bpf_obj, "rx_stats");
    if (!tx_map || !rx_map) {
        fprintf(stderr, "ERROR: finding maps in BPF object failed\n");
        goto cleanup;
    }

    // 5. 5秒ごとにマップの内容を表示するループ
    while (!exiting) {
        sleep(5);
        print_stats(tx_map, rx_map);
    }

cleanup:
    // 6. クリーンアップ
    printf("Detaching and cleaning up...\n");
    if (links[0]) bpf_link__destroy(links[0]);
    if (links[1]) bpf_link__destroy(links[1]);
    bpf_object__close(bpf_obj);
    printf("Done.\n");

    return 0;
}

Makefile

eBPFプログラムとそのユーザー空間プログラムのコンパイルを自動化します。

CLANG = clang
GCC = gcc
BPF_CFLAGS = -g -O2 -target bpf -c -D__TARGET_ARCH_x86
CHECKER_CFLAGS = -g -Wall
CHECKER_LDFLAGS = -lbpf

# ソースファイルと出力ファイル
BPF_SRC = agent.c
BPF_OBJ = agent.o
CHECKER_SRC = agent_checker.c
CHECKER_BIN = agent_checker
VMLINUX_H = vmlinux.h
COMMON_HEADER = bpf_common.h

.PHONY: all clean checker

all: $(VMLINUX_H) $(BPF_OBJ) $(CHECKER_BIN)

# vmlinux.hの生成
# vmlinux.hをincludeすることで、eBPFプログラムはカーネルの型定義を参照できるようになります
$(VMLINUX_H):
	bpftool btf dump file /sys/kernel/btf/vmlinux format c > $(VMLINUX_H)

# eBPFプログラム(agent.c)をオブジェクトファイル(agent.o)にコンパイル
$(BPF_OBJ): $(BPF_SRC) $(VMLINUX_H) $(COMMON_HEADER)
	$(CLANG) $(BPF_CFLAGS) -I. $(BPF_SRC) -o $(BPF_OBJ)

# ユーザー空間のチェッカープログラムをコンパイル
$(CHECKER_BIN): $(CHECKER_SRC) $(COMMON_HEADER)
	$(GCC) $(CHECKER_CFLAGS) $(CHECKER_SRC) -o $(CHECKER_BIN) $(CHECKER_LDFLAGS)

checker: $(CHECKER_BIN)

clean:
	rm -f $(BPF_OBJ) $(CHECKER_BIN) $(VMLINUX_H)

コンパイル

Makefileを実行します。

$ sudo make clean all
rm -f agent.o ../agent_checker vmlinux.h
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
clang -g -O2 -target bpf -c -D__TARGET_ARCH_x86 -I. agent.c -o agent.o
gcc -g -Wall agent_checker.c -o ../agent_checker -lbpf

agent_checkerが作成されました。

$ ls
Makefile  agent.c  agent.o  agent_checker  agent_checker.c  bpf_common.h  vmlinux.h

eBPFプログラムの実行

agent_checkerを実行します。

$ sudo ./agent_checker 
Successfully loaded and attached BPF programs. Watching TCP traffic...
Press Ctrl+C to exit.

別ターミナルからgoogle.com宛にcurlを実行します。

$ curl -LI google.com

google.com宛の通信が表示されました。142.250.198.14のIPを含む通信が該当します。

--- TX Stats ---
  10.146.0.35 -> 59.132.57.185 : 3696 bytes
--- RX Stats ---
  59.132.57.185 -> 10.146.0.35 : 3564 bytes

...

--- TX Stats ---
  10.146.0.35 -> 169.254.169.254 : 454 bytes
  10.146.0.35 -> 142.250.198.14 : 74 bytes
  10.146.0.35 -> 59.132.57.185 : 36339 bytes
  10.146.0.35 -> 216.58.220.132 : 78 bytes
--- RX Stats ---
  142.250.198.14 -> 10.146.0.35 : 554 bytes
  59.132.57.185 -> 10.146.0.35 : 30446 bytes
  169.254.169.254 -> 10.146.0.35 : 12896 bytes
  216.58.220.132 -> 10.146.0.35 : 1050 bytes

終わりに

カーネルのTCP送受信をフックして、通信量を標準出力するeBPFプログラムを実行できました。eBPFプログラム特有のヘルパー関数やトレースポイント、フックするカーネル関数の実装など見るべきポイントが多く、慣れが必要だなと感じました。
eBPFプログラムはLinuxカーネル内部で発生するほぼ全てのイベントをフックすることができるため、選択肢として持っておくと非常に便利ですね。
次回は、Kubernetes環境でeBPFプログラムを実行してみたいと思います。

参考情報

以下、参考情報を記載しています。

eBPFプログラムから参照している関数について

今回利用した関数やデータ型の詳細は、以下のURLから確認して下さい。

eBPFプログラムの設計

eBPFプログラムは、なるべくシンプルで高速な処理となるように設計する必要があります。eBPFプログラムの処理が遅いとカーネル全体の動作が遅延し、システム全体のパフォーマンスに深刻な影響を与えてしまいます。
また、Linuxカーネルを保護するためにeBPFプログラムには様々な制約が設けられています。

__sync_fetch_and_add について

__sync_fetch_and_addとは、GCCやClangコンパイラが提供するアトミック操作のための特殊な組み込み関数(Built-in Function)です。

eBPFプログラムは、複数のCPUコアで同時に実行される可能性があります。__sync_fetch_and_add関数は、処理の競合(レースコンディション)が発生しないように値の更新をアトミックに行います。これにより、複数CPUからの同時アクセスが発生しても安全にカウンターを増やすことができます。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?