ubuntu 22.04にFD.io VPP v23.02を使ってNAPT Gatewayを構築した時のメモ

# はじめに

APU6を2台入手したので、SFPモジュールを介してLC光ケーブルで接続しつつ、Linux FoundationがホストするFast Data Project (FD.io)のVPPを試すことにしました。

* [Wikipedia - Vector Packet Processing](https://en.wikipedia.org/wiki/Vector_Packet_Processing)

英語でも資料が十分とはいえず、いろいろ問題に遭遇したので作業時のメモを残しておくことにします。

* [fd.io公式ガイド - VPP v23.06](https://s3-docs.fd.io/vpp/23.06/)

# 環境

* OS - Ubuntu 22.04
* Software - FD.io VPP v23.02
* Hardware - APU6 with SFP modules (10Gtek製 1.25G 850nm MMF) 2台

## 目標とする構成とVPPの特徴

構成は一般的なホームルーターと似ていますが、グローバルIPは静的に固定します。
このためIPoEやPPPoEは使いませんし、試していません。IPoEは問題ないと思いますが、PPPoEもPluginにpppoe_plugin.soが含まれているので何とかなりそうです。

:::note
VPPを使ったIPoE接続については別記事にまとめています。
[FD.io VPP 24.02でフレッツ(IPoE)ホームルーターを自作した時のメモ](https://qiita.com/YasuhiroABE/items/9c2b295f72a38ed22ed1)
:::

本家の資料にも[Home gatewayを構築する設定例(s3-docs.fd.io)](https://s3-docs.fd.io/vpp/23.02/usecases/home_gateway.html)が掲載されていますが、ここで紹介されているグローバルIPを取得するためのDHCPクライアントは使用しませんので、それらの設定も省略しています。

192.168.110.0/24には実験用のサーバーなどを配置する予定で、うまく動作すればAPU6#01にdnsmasqを導入し、192.168.110.10経由でネットワーク全体の管理を行います。

![20231113_fd.IO-vpp-napt-gateway.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/4efb5625-479e-0337-57e5-3bc713c94353.png)


APU6#01で稼動するsshdやnginxなどのOS配下のプロセスは、VPPの管理下にあるGlobal IP(xxx.xxx.xxx.xxx)には接続できないため、kernelの配下にあるtap0(192.168.110.10)を通してVPP管理下にあるL2ブリッジ(bvi1)を経由して192.168.110.0/24の物理ネットワークとインターネットに接続します。

VPPが特殊なのはUserlandで動作しているため、デバイスが一般的なカーネル管理下になくiptablesなどでパケットをフィルタリングすることはもちろん、``ip addr``コマンドでデバイス情報を表示することすらできない点にあります。

VPPが動作した状態では``ip addr``コマンドはloとtap0デバイスのみを表示します。

tap0はuserlandで動作するVPPとカーネルが管理する一般的なネットワーク通信を仲介する役割を担当します。OSからはグローバルIP(xxx.xxx.xxx.xxx)がみえないため、運用に必要なパッケージの更新などでインターネットに接続する際にはtap0インタフェースを通じて、NAPTによってVPPのグローバルIPを通り外部と通信することになります。

sshdなどのdaemonは、tap0デバイスの192.168.110.10/24にbindし動作します。外部からsshdにアクセスしたい場合にはVPPの設定でグローバルIPからtap0にポートフォワードすることになります。Webサーバーも同様でVPPからフォワードされたパケットをtap0を通じて受け取ります。

この時動作するサーバーは実際には0.0.0.0にbindしないと起動順によっては192.168.110.10/24が未設定となって起動できずエラーになる可能性があります。

もし192.168.110.10にbindするように設定したい場合には起動に失敗しても再起動するよう、適当な監視スクリプトをcrontabなどに指定しておくのが良いでしょう。

## 導入方法

公式ドキュメントの手順に従って、リリース版のdebパッケージを導入しています。

installスクリプトは、/etc/apt/sources.list.d/ と /etc/apt/keyrings/ に、それぞれファイルを配置します。ansibleで自動化する際には、このファイルの内容をコピーして配置すればインストールスクリプトを実行する必要はありません。

```bash:vpp関連パッケージ
$ dpkg -l | grep vpp
ii  libvppinfra                           23.02-release                           amd64        Vector Packet Processing--runtime libraries
ii  vpp                                   23.02-release                           amd64        Vector Packet Processing--executables
ii  vpp-plugin-core                       23.02-release                           amd64        Vector Packet Processing--runtime core plugins
ii  vpp-plugin-dpdk                       23.02-release                           amd64        Vector Packet Processing--runtime dpdk plugin
```

# 参考資料

* [公式ガイド - VPP as a Home Gateway](https://s3-docs.fd.io/vpp/23.02/usecases/home_gateway.html)
* [メーリングリスト Re: [vpp-dev] nat44 not working under Ubuntu 22.04](https://www.mail-archive.com/vpp-dev@lists.fd.io/msg18169.html)
* [公式ガイド - NAT44-ED: NAT44 Endpoint Dependent](https://s3-docs.fd.io/vpp/23.02/developer/plugins/nat44_ed_doc.html)

この他の参照した資料

* https://www.slideshare.net/tetsusat/vpp-147934534 (FD.io VPP事始め)
* https://haryachyy.wordpress.com/tag/vector-packet-processing/ (Denys Haryachyy Blog)
* https://blog.apnic.net/2020/04/17/kernel-bypass-networking-with-fd-io-and-vpp/
* https://metonymical.hatenablog.com/entry/2020/08/10/005408


# 基本的な設定

基本的には公式サイトのガイドと同様ですが、微妙な書き方の違いで挙動が異なるため、検証は慎重に行う必要がありました。

```text:/etc/vpp/startup.conf
unix {
  nodaemon
  log /var/log/vpp/vpp.log
  full-coredump
  cli-listen /run/vpp/cli.sock

  startup-config /etc/vpp/local.cfg
}

api-trace {
  on
}

api-segment {
  gid vpp
}

socksvr {
  default
}

cpu {
        main-core 1
        corelist-workers 2-3
}

dpdk {
        dev default {
                num-rx-desc 512
                num-tx-desc 512
        }
        no-multi-seg
        no-tx-checksum-offload

        dev 0000:01:00.0 {
                name enp1s0
                num-rx-queues 2
                num-tx-queues 2
        }
        dev 0000:02:00.0 {
                name enp2s0
                num-rx-queues 2
                num-tx-queues 2
        }
}

plugins {
  plugin default { disable } ## default
  plugin nat_plugin.so { enable }
  plugin dpdk_plugin.so { enable }
}
```

dpdk{}のdevに指定するのはpci-eのアドレスです。``lshw -c network``などで表示させることができます。

```text:/etc/vpp/local.cfg
comment { for local network }
bvi create instance 0
set int l2 bridge bvi0 1 bvi
set int ip address bvi0 192.168.110.1/24
set int state bvi0 up

comment { Add more inside interfaces as needed ... }
set int l2 bridge enp1s0 1
set int state enp1s0 up

comment { for enp2s0, global ip }
set int ip address enp2s0 xxx.xxx.xxx.xxx/24
set int state enp2s0 up
ip route add 0.0.0.0/0 via xxx.xxx.xxx.1 enp2s0

comment { host-stack access, tap0 }
create tap host-if-name tap0 host-ip4-addr 192.168.110.10/24 host-ip4-gw 192.168.110.1
set int l2 bridge tap0 1
set int state tap0 up

comment { Configure NAT }
nat44 plugin enable sessions 63000
nat44 add interface address enp2s0
set interface nat44 in bvi0
set interface nat44 out enp2s0 output-feature
nat44 forwarding enable

comment { allow inbound ssh to the 22 }
nat44 add static mapping local 192.168.110.10 22 external enp2s0 22 tcp
```

後述しますが、nat44のルールを書く際には公式ガイドなどにある in, out を1行にまとめる書き方では動作しませんでした。よくみると公式ガイドでは``output-feature``は省略されていたりするので、公式ガイドが完全に間違っているわけではありません。

意味を考えながら必要な手続きを順序良く記述する必要がある点に注意が必要です。

# 戸惑った事や遭遇した不具合

検索して出てくる資料そのものが少かったり、ターゲットがvSwitchのような用途だったり、使用したバージョンが古く、``nat { endpoint-dependent }`` のような23.02ではunknownとしてエラーになる設定を含んだりしました。

例えば、[Denys Haryachyy Blog](https://haryachyy.wordpress.com/tag/vector-packet-processing/)には次のようなNAT設定が掲載されていますが、これは現在(v23.02)ではまったく動作しません。

```text:NAT設定例
nat {
    endpoint-dependent
    translation hash buckets 1048576
    translation hash memory 268435456
    user hash buckets 1024
    max translations per user 10000
 }
```

またunix{}内部(あるいはstartup-configに指定されるファイル)に記述する、次の設定も23.02ではエラーになります。

```text:エラーになる設定例
nat addr-port-assignment-alg default
```

この他に遭遇した事柄について、まとめています。

## ssh接続での作業が難しい

OSをインストールした直後には、``ip addr``で表示される一般的なネットワークデバイスを利用しなければいけません。

この初期状態からVPPを導入するなどのネットワーク関連の設定を行うことは、通信が切断されるなど作業が継続困難になる可能性がそれなりにあります。

BMCカードなどが利用できれば問題ないのですが、今回利用したAPU6はCP2104チップを利用したシリアル接続用のUSBポートが準備されています。Windowsではどうも動作が不安定だったり、VMware上のLinuxホストに接続できないなどの不具合が頻発したので、カーネルにドライバが組込まれているlinuxを利用しました。Linuxホストでも度々通信が不安定になっています。

それでもWindowsよりはいくらか安定しているので、他のLinuxホストにssh経由で接続した後に、``sudo screen /dev/ttyUSB0 115200`` といった方法でシリアル端末にアクセスしています。

UbuntuをホストとしてVPPを利用する場合にはホームディレクトリにnetplanの設定ファイルを置いておいて必要に応じて、/etc/netplan/に配置して再起動やnetplan applyすることでカーネルとVPPの間を行き来しました。

作業環境については、BMCカードなどを利用するか、SSH接続可能な端末からシリアル経由でVPPが稼動するサーバーにアクセスする方法を検討・確認しておくことがお勧めです。

## sudo vppctl show interface で目的のNICが表示されない

そもそもカーネル管理下にあって、netplanなどでIPアドレスなどが割り当てられているNICはVPPで扱うことができません。

様々な例でもipコマンドでlink downさせてからVPPの設定を始めています。

ubuntu 22.04 server版ではnetplanの設定ファイルを削除したりsystemctlからvpp.serviceをdisableにするなどして切り替えれば問題ないのですが、Desktop版などを利用するとnetworkmanagerなどが動作してしまいかなりはまるポイントだろうと思います。

OS管理下にないインタフェースは自動的にVPPに表示されるので、MACアドレスやPCIのバス番号などで識別することが必要です。

```bash:Device名やBUS番号の調べ方
$ sudo lshw -c network
$ sudo lshw -c network -businfo
```

## どのpluginを読み込めば良いかわからない

nat関係と思われるpluginには次のようなものがあります。

```bash:
$ $ ls /usr/lib/x86_64-linux-gnu/vpp_plugins | grep nat
cnat_plugin.so
crypto_native_plugin.so
nat44_ei_plugin.so
nat64_plugin.so
nat66_plugin.so
nat_plugin.so
pnat_plugin.so
```

一般的なgatewayを構築するだけであれば、nat_plugin.so だけで十分です。

マニュアルなどをみる限り、どのpluginにどんな命令が定義されているかは、``nm -D --defined-only``や``strings``などのコマンドで個別に確認するのが確実そうです。



## nat44が有効にできない

やや古い公式のHome Gatewayの設定例を参考にしたところ、以下のようなエラーメッセージが表示されてしまいました。

```
Mar 25 22:49:20 ub2204 vnet[693]: nat: nat44 is disabled
Mar 25 22:49:20 ub2204 vnet[693]: nat44 add interface address: add enp2s0 address failed
```

参考資料にも挙げている[メーリングリストの記事](https://www.mail-archive.com/vpp-dev@lists.fd.io/msg18169.html)から、``nat44 plugin enable`` が抜けている場合のあることが分かります。

利用するVPPのバージョンに合わせた[NAT44-EDのドキュメント(https://s3-docs.fd.io/vpp/23.02/developer/plugins/nat44_ed_doc.html)](https://s3-docs.fd.io/vpp/23.02/developer/plugins/nat44_ed_doc.html)を確認することが正確な情報を入手するために必要です。

また、Debian/Ubuntuのdebパッケージのデフォルト設定はpluginが読み込まれないようになっているため、nat関連のパッケージを読み込む必要があります。

```/et/vpp/startup.confに追加したplugin設定
plugins {
  plugin nat_plugin.so { enable }
}
```

nat_plugin.soにはnat44関連の命令がほとんど含まれています。

最終的にはpluginを追加し、次のような命令をnat44の設定を開始する先頭に配置して解決しました。

```unix{}内に追加したplugin設定
nat44 plugin enable sessions 63000
```

nat関連の設定は、nat44 plugin enable 〜を実行してから記述しましょう。

## NATを通じて、DNSはlookupできるのにTCP接続ができない

NATを正しく構成したようにみえても、TCPでの通信が成功しませんでした。

DNSは参照できており、UDPの通信で単独パケットのやりとりは問題ないようです。

```bash:DNSのパケット(UDP)は戻ってくるらしい
(192.168.110.2) $ dig www.ibm.com

...
;; ANSWER SECTION:
www.ibm.com.            183     IN      CNAME   www.ibm.com.cs186.net.
www.ibm.com.cs186.net.  1143    IN      CNAME   outer-global-dual.ibmcom-tls12.edgekey.net.
outer-global-dual.ibmcom-tls12.edgekey.net. 14200 IN CNAME e7817.dscx.akamaiedge.net.
e7817.dscx.akamaiedge.net. 16   IN      A       104.78.105.90
...
```

UDPのパケットは問題ないですが、TCPでの通信は成功しません。

```bash:TCP接続は続かず失敗してしまう

(192.168.110.2) $ curl -vv www.ibm.com
*   Trying 104.78.105.90:80...
* Connected to www.ibm.com (104.78.105.90) port 80 (#0)
> GET / HTTP/1.1
> Host: www.ibm.com
> User-Agent: curl/7.81.0
> Accept: */*
> 
## このまま停止
```

自分が管理できるWebサーバーに接続させてみると、SYNが届いていて、ACKを返しているのに届いていないようです。TCPのハンドシェイクで問題が起るのはセッション管理がおかしいようですが、関連した話題はメーリングリストなどではみつけられませんでした。

* https://lists.fd.io/g/vpp-dev/topic/38876666

```bash:Webサーバー側のtcpdumpの出力
$ sudo tcpdump -n -vvv -i ens1 port 80
tcpdump: listening on ens1, link-type EN10MB (Ethernet), capture size 262144 bytes
12:39:58.980997 IP (tos 0x0, ttl 63, id 40517, offset 0, flags [DF], proto TCP (6), length 60)
    xxx.xxx.xxx.xxx.16961 > yyy.yyy.yyy.yyy.http: Flags [S], cksum 0x5bff (correct), seq 1576809263, win 64240
, options [mss 1460,sackOK,TS val 3377869175 ecr 0,nop,wscale 7], length 0
12:39:58.981073 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    yyy.yyy.yyy.yyy.http > xxx.xxx.xxx.xxx.16961: Flags [S.], cksum 0x22b4 (incorrect -> 0x1d5f), seq 23420096
20, ack 1576809264, win 28960, options [mss 1460,sackOK,TS val 984925950 ecr 3377869175,nop,wscale 7], length 0
```

試行錯誤の結果、local.cfgファイルの書き方を変更することで解決しました。

```text:問題のある記述
set interface nat44 in bvi0 out enp2s0 output-feature
```

これを次のように変更すると問題は解消しました。

```text:問題が解決した記述
set interface nat44 in bvi0
set interface nat44 out enp2s0 output-feature
```

この違いについては追求しきれていません。→ 最後に原因を追記しました。

## local.cfgの記述で気をつけるべき点

local.cfgファイルはstartup.confの中で、unix{}内部でstartup-configによって読み込むようにしている設定ファイルです。

この設定ファイルの書式はバージョンによって、かなり変更されているようです。

* https://fdio-vpp.readthedocs.io/en/latest/usecases/homegateway.html (記述内容は古いため要注意)

例えば、v:latestタグで表示される古い公式サイトでは次のような記述がありますが、これはv23.02では動作しません。

```text:latestとして記述されている設定
dpdk {
  ...
  poll-sleep 10
}
```

ちゃんと公式ガイドを参照すると、これはunix{}のセクションに、``poll-sleep-usec``として設定ができます。

```text:v23.02で動作する
unix {
  ...
  poll-sleep-usec 100
}
```

このように現在のバージョンでは有効ではない記述がとにかく多い点を念頭にメーリングリストのアーカイブなどを検索する必要がありました。

# さいごに

nat44のおかしな挙動はVMware Workstation上のVMのNICのドライバをvmxnet3に変更して検証しましたが、同様の挙動を示しました。確認のため23.02系列の最新版や23.06などのmasterブランチのパッケージで確認しても同様でした。

VPPを利用するユーザーの多くはスループットやvSwitchなどホスト内の複雑なルーティングを行いたい人達がまだ多いのかもしれませんが、nat44は実用上では重要かつ基本的なモジュールです。ドキュメントや利用例の報告が少ないのは、まだ従来の用途を置き換えるような目的では利用されていないからなのかもしれません。

しかしUserlandでルーティングが操作できることは、OSの制約からネットワークを分離できるためとても魅力的です。

OSが自身の更新のためにtapデバイスが必要だったりする点は面倒という印象を強く与えると思います。それでもメリットが大きいと思われますので、今後はこういった仕組みを使って個人的な実験に利用しているNAPT Gateway以外のサーバーも置き換えていくつもりです。


# 後日談 〜 nat44 in out output-feature を一行で書いて問題が起きた理由

すっきりしなかった、output-featureを含む行を、2行に分けると問題が収束した理由についての説明です。

問題が発生したものの、次のような設定にしたところ問題が収束したところまでは分かっていました。

```text:問題が解決した記述
set interface nat44 in bvi0
set interface nat44 out enp2s0 output-feature
```

これはgithubにホストされているVPPのコードを確認すると、内部では次のように1つの関数内で処理されています。

```c:src/plugins/nat/nat44-ed/nat44_ed_cli.cからの抜粋
static clib_error_t *
snat_feature_command_fn (vlib_main_t * vm,
			 unformat_input_t * input, vlib_cli_command_t * cmd)
{
...
  u32 *inside_sw_if_indices = 0;
  u32 *outside_sw_if_indices = 0;
  u8 is_output_feature = 0;
...
      if (unformat (line_input, "in %U", unformat_vnet_sw_interface,
		    vnm, &sw_if_index))
	vec_add1 (inside_sw_if_indices, sw_if_index);
      else if (unformat (line_input, "out %U", unformat_vnet_sw_interface,
			 vnm, &sw_if_index))
	vec_add1 (outside_sw_if_indices, sw_if_index);
      else if (unformat (line_input, "output-feature"))
	is_output_feature = 1;
...
```

一見すると、1行でも2行に分けても同様に処理されそうですが、その先のコードでは次のように分岐しています。

```c:in_%Uを処理するコード
...
      for (i = 0; i < vec_len (inside_sw_if_indices); i++)
	{
	  sw_if_index = inside_sw_if_indices[i];
	  if (is_output_feature)
	    {
...
		{
		  rv = nat44_ed_add_output_interface (sw_if_index);
		}
...
	    }
	  else
	    {
...
		{
		  rv = nat44_ed_add_interface (sw_if_index, 1);
		}
...
```

抜粋が多くて分かりにくいですが、inデバイスについても``is_ouptput_feature``の有無によって呼ばれるコードが違います。

``output-feature``を指定すると、そのデバイスがnat44_ed_add_output_interfaceに渡されますが、この挙動は、後半の``outside_sw_if_indices``でも同様に呼ばれます。

今回の場合はtap0と接続する内部のbvi0デバイスをnat44_ed_add_output_interface()を通じて登録することになるので、なんとなく外部IPもmasqueradeされそうな雰囲気です。

もう少し調べると確かに一行で``in out output-feature``を書き並べた場合は、いわゆるヘアピンNATとも違い、外部から内部にアクセスする際のIPアドレスもenp2s0に付与されているIPアドレス(xxx.xxx.xxx.xxx)でmasqueradeされてしまっていました。どこからsshdにログインしても接続元は(xxx.xxx.xxx.xxx)になってしまいます。

失敗した時と、成功した時での``$ sudo vppctl show int bvi0 features``コマンドの出力は次のように違いがあります。

```text:失敗した時のbvi0のfeatureの抜粋
ip4-local:
  none configured

ip4-output:
  ip4-sv-reassembly-output-feature
  nat44-in2out-output-worker-handoff

ip4-multicast:
  none configured

ip4-unicast:
  ip4-sv-reassembly-feature
  nat44-out2in-worker-handoff
```

```text:成功した時のbvi0のfeatureの抜粋
ip4-local:       
  none configured
                 
ip4-output:      
  none configured
             
ip4-multicast:   
  none configured
         
ip4-unicast:              
  ip4-sv-reassembly-feature 
  nat44-in2out-worker-handoff
```

bvi0の``ip4-output:``の部分が違います。

この失敗した時の表示は、enp2s0の外部IFに設定されているものと同じでした。

```text:成功した時のenp2s0のfeaturesの抜粋
ip4-local:
  none configured
                 
ip4-output:
  ip4-sv-reassembly-output-feature
  nat44-in2out-output-worker-handoff

ip4-multicast:
  none configured

ip4-unicast:  
  ip4-sv-reassembly-feature
  nat44-out2in-worker-handoff
```

このようにnat44の設定をする際には、output-featureを設定する場合には自動的にoutに指定したデバイスにだけ反映されるということはないので、in,outの設定は分けて記述するようにするのが良さそうです。

ここまでで、挙動が違う理由については判明したかなと思います。


# 後日談2 〜 VPPを利用する際のメモリサイズとCPUのキャパプラについて

APU6上でVPPを動作させたまではよくて、その分には問題ないのですが、4GBのメモリでは少し窮屈だと感じるようになりました。

起動から一定時間経過すると平常時は800MB程度の空きメモリはありますが、venvを経由してkubesprayに必要なパッケージを導入しようと、``pip install -r requirements.txt`` を実行すると100MB以下まで空きメモリが逼迫してしまいます。

APU6がVPP専用ノードであれば問題ないのですが、nginxやdnsmasqなどいくらか追加のワークロードも稼動させたいため、メモリに余裕がない状態は少し厳しいです。

またtopコマンドで確認するとvpp_mainプロセスのCPU使用率は200%を越えている状況です。AMD GX-412TC CPUは4スレッド動作させることを考えるとまだ余裕はありますが、全体的なパフォーマンスには余裕がない状態が続いています。

PC Engines社から2023年6月末でGX-412TCの受注停止がアナウンスされています。約1年程度の需要を満たすだけの最終オーダーを行った上で製造を継続する意向が表明されていますが、後継製品がアナウンスされていない状況では今後はProtectli社製のFanlessサーバーなどを購入すると思います。

Protectli社の製品は、いくらか高価でCPU性能は価格に比例して変化しますが、APUよりは高性能で、メモリは確実に増やすことができます。とりあえずVP2420を購入して32GBほどメモリを搭載して同様の構成でテストする予定です。

以上