CUBE SUGAR CONTAINER

技術系のこと書きます。

Linux の Network Namespace で L2TP (IPsec なし) を試す

今回は L2TP の LAC (L2TP Access Concentrator) と LNS (L2TP Network Server) を Linux の Network Namespace の環境で動かしてみる。 L2TP はリモートアクセス VPN でよく使われるプロトコルの一つ。 ただし、今回は IPsec を組み合わせないため、単なるトンネリングプロトコルとしての検証になる。 LAC は接続を開始するクライアント、LNS は接続を待ち受けるサーバと考えれば良い。

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.2 LTS"
$ uname -srm
Linux 5.15.0-71-generic x86_64
$ xl2tpd --version

xl2tpd version:  xl2tpd-1.3.16

もくじ

下準備

まずは必要なパッケージをインストールする。 L2TP の LAC / LNS としては xl2tpd(8) を使う。

$ sudo apt-get update
$ sudo apt-get install xl2tpd iputils-ping

インストールすると、その時点で xl2tpd のサービスが稼働してしまうため止めておく。

$ sudo systemctl stop xl2tpd
$ sudo systemctl disable xl2tpd

ネットワークを構築する

今回は 3 つの Network Namespace を使う。 laclns はそのまま LAC と LNS に対応する。 remote についてはリモートアクセス VPN をつないだ先にあるネットワークの適当なサーバとでも考えてもらえれば良い。

$ sudo ip netns add lac
$ sudo ip netns add lns
$ sudo ip netns add remote

続いては LAC と LNS の間をつなぐ。 ここは 203.0.113.0/24 のセグメントにする。

$ sudo ip link add lac-veth0 type veth peer name lns-veth0
$ sudo ip link set lac-veth0 netns lac
$ sudo ip link set lns-veth0 netns lns
$ sudo ip netns exec lac ip link set lac-veth0 up
$ sudo ip netns exec lns ip link set lns-veth0 up
$ sudo ip netns exec lac ip address add 203.0.113.1/24 dev lac-veth0
$ sudo ip netns exec lns ip address add 203.0.113.254/24 dev lns-veth0

そして LNS と remote の間をつなぐ。 ここは 192.0.2.0/24 のセグメントにする。

$ sudo ip link add lns-veth1 type veth peer name remote-veth0
$ sudo ip link set lns-veth1 netns lns
$ sudo ip link set remote-veth0 netns remote
$ sudo ip netns exec lns ip link set lns-veth1 up
$ sudo ip netns exec remote ip link set remote-veth0 up
$ sudo ip netns exec lns ip address add 192.0.2.254/24 dev lns-veth1
$ sudo ip netns exec remote ip address add 192.0.2.1/24 dev remote-veth0

LNS はルータとして機能させる。

$ sudo ip netns exec lns sysctl net.ipv4.ip_forward=1

remote にはデフォルトルートとして LNS のアドレスを設定しておく。

$ sudo ip netns exec remote ip route add default via 192.0.2.254

これでネットワークができた。

LNS をセットアップする

まずは LNS に必要な設定を追加していく。 xl2tpd の設定ファイルを用意する。 global セクションの debug から始まる項目については表示される内容が変わるだけなのでお好みで。

$ cat << 'EOF' | sudo tee /etc/xl2tpd/xl2tpd.lns.conf > /dev/null
[global]
port = 1701
access control = no
debug avp = no
debug network = yes
debug packet = no
debug state = yes
debug tunnel = yes

[lns default]
ip range = 198.51.100.10-198.51.100.20
local ip = 198.51.100.1
require authentication = yes
name = example
pppoptfile = /etc/ppp/options.l2tpd.lns
ppp debug = yes
EOF

ポイントとしては lns セクションの名前を default にするところ。 また、local ip は LNS のトンネル終端アドレス、ip range が LAC のトンネル終端アドレスになる。

次に pppd(8) の設定ファイルを用意する。 xl2ptd(8) は pppd(8) と連携して動作する。

$ cat << 'EOF' | sudo tee /etc/ppp/options.l2tpd.lns > /dev/null
require-mschap-v2
debug
logfile /var/log/pppd.lns.log
EOF

このとき、アカウントの認証方式としては MS-CHAPv2 を指定しておく。

pppd(8) が認証に使う、アカウント情報を記載した設定ファイルを用意する。 ここでは testuser というユーザ名で testpassword のパスワードでログインするユーザを作った。

$ cat << 'EOF' | sudo tee /etc/ppp/chap-secrets > /dev/null
testuser * testpassword *
EOF

これで LNS に必要な設定は揃った。 設定ファイルを指定して xl2tpd を起動する。 このとき -D オプションをつけるとデーモンにならずフォアグラウンドで動作する。

$ sudo ip netns exec lns \
    xl2tpd -D \
      -c /etc/xl2tpd/xl2tpd.lns.conf \
      -p /var/run/xl2tpd.lns.pid \
      -C /var/run/xl2tpd/l2tp-control.lns

LAC をセットアップする

続いては LAC をセットアップする。

先ほどと同じように xl2tpd の設定ファイルを用意する。

$ cat << 'EOF' | sudo tee /etc/xl2tpd/xl2tpd.lac.conf > /dev/null
[global]
port = 1701
access control = no
rand source = dev
debug avp = no
debug network = yes
debug packet = no
debug state = yes
debug tunnel = yes

[lac mylac]
lns = 203.0.113.254
require chap = yes
refuse pap = yes
require authentication = yes
pppoptfile = /etc/ppp/options.l2tpd.lac
ppp debug = yes
EOF

ポイントとしては lac セクションの名前に default 以外をつける。 そして lns には LNS が待ち受けている IP アドレスを指定する。

続いて、先ほどと同様に pppd(8) の設定ファイルを用意する。 namepassword では、先ほど設定したアカウントの情報を使う。

$ cat << 'EOF' | sudo tee /etc/ppp/options.l2tpd.lac > /dev/null
defaultroute
noauth
debug
logfile /var/log/pppd.lac.log
name "testuser"
password "testpassword"
EOF

これで LAC の設定が終わった。 設定ファイルを指定して xl2tpd を起動する。

$ sudo ip netns exec lac \
    xl2tpd -D \
      -c /etc/xl2tpd/xl2tpd.lac.conf \
      -p /var/run/xl2tpd.lac.pid \
      -C /var/run/xl2tpd/l2tp-control.lac

トンネルを作る

そして xl2tpd-control(8) を使って LAC にコネクションを開始するように指示する。 このとき -c オプションで LAC 側の xl2tpd に指定した制御用のソケットファイルを指定する。 また、引数には先ほど xl2tpd の設定ファイルで lac セクションの名前に設定したものを入力する。

$ sudo xl2tpd-control -c /var/run/xl2tpd/l2tp-control.lac connect-lac mylac

上手くいけば次のようにトンネルインターフェイス (ppp0) が作成されて、トンネル終端アドレスが割り振られる。

$ sudo ip netns exec lac ip address show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ppp0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 3
    link/ppp 
    inet 198.51.100.10 peer 198.51.100.1/32 scope global ppp0
       valid_lft forever preferred_lft forever
    inet6 fe80::4cad:b8b3:fb08:36cd peer fe80::9090:8d17:86c2:3f4e/128 scope link 
       valid_lft forever preferred_lft forever
10: lac-veth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:01 brd ff:ff:ff:ff:ff:ff link-netns lns
    inet 203.0.113.1/24 scope global lac-veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::200:5eff:fe00:5301/64 scope link 
       valid_lft forever preferred_lft forever

ルーティングテーブルを確認すると、デフォルトルートがトンネルインターフェイスに向いている。

$ sudo ip netns exec lac ip route show
default dev ppp0 scope link 
198.51.100.1 dev ppp0 proto kernel scope link src 198.51.100.10 
203.0.113.0/24 dev lac-veth0 proto kernel scope link src 203.0.113.1 

試しに LAC から remote の IP アドレスに ping を打ってみると、ちゃんと疎通がある。

$ sudo ip netns exec lac ping -c 3 192.0.2.1 -I 198.51.100.10
PING 192.0.2.1 (192.0.2.1) from 198.51.100.10 : 56(84) bytes of data.
64 bytes from 192.0.2.1: icmp_seq=1 ttl=63 time=1.91 ms
64 bytes from 192.0.2.1: icmp_seq=2 ttl=63 time=1.49 ms
64 bytes from 192.0.2.1: icmp_seq=3 ttl=63 time=1.44 ms

--- 192.0.2.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 1.441/1.612/1.906/0.208 ms

なお、xl2tpd-control connect-lac するときに tcpdump(1) を使うと L2TP のやり取りが確認できる。

めでたしめでたし。

参考

manpages.ubuntu.com

manpages.ubuntu.com

datatracker.ietf.org

Linux の Network Namespace で PPPoE を試す

今回は PPPoE (PPP over Ethernet) のサーバとクライアントを Network Namespace の環境で動かしてみる。 PPPoE は、その名のとおり PPP (Point-to-Point Protocol) を Ethernet で使えるようにしたもの。 NTT 東西が提供するブロードバンドサービスのフレッツで、ユーザの認証や設定のために使われていることで有名なプロトコル。

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.2 LTS"
$ uname -srm
Linux 5.15.0-71-generic aarch64

もくじ

下準備

まずは必要なパッケージをインストールしておく。

$ sudo apt-get update
$ sudo apt-get -y install pppoe pppoeconf

ネットワークを構築する

今回は routerprovider という 2 つの Network Namespace を作る。 router がユーザの自宅にあるブロードバンドルータを想定している。 同様に provider が ISP の AC (Access Concentrator) を想定している。 ようするに router がクライアントで provider がサーバということ。

$ sudo ip netns add router
$ sudo ip netns add provider

Network Namespace 同士を接続するのに Virtual Ethernet デバイスのインターフェイスを用意する。

$ sudo ip link add wan-veth0 type veth peer name gw-veth0
$ sudo ip link set wan-veth0 netns provider
$ sudo ip link set gw-veth0 netns router

それぞれのインターフェイスに MAC アドレスを指定した上でリンクアップさせる。

$ sudo ip netns exec provider ip link set wan-veth0 address 00:00:5E:00:53:01
$ sudo ip netns exec provider ip link set wan-veth0 up
$ sudo ip netns exec router ip link set gw-veth0 address 00:00:5E:00:53:02
$ sudo ip netns exec router ip link set gw-veth0 up

サーバ側のインターフェイスには IP アドレスを付与しておく。

$ sudo ip netns exec provider ip address add 203.0.113.254/24 dev wan-veth0

サーバをセットアップする

まずは PPPoE のサーバをセットアップする。

PPPoE サーバの設定ファイルを次のように用意する。 認証には CHAP (Challenge-Handshake Authentication Protocol) を使う。

$ cat << 'EOF' | sudo tee /etc/ppp/pppoe-server-options > /dev/null
require-chap
lcp-echo-interval 60
lcp-echo-failure 5
debug
logfile /var/log/pppd.log
EOF

続いて、ユーザのアカウントを設定ファイルに記述する。 以下では testuser というユーザ名で testpassword というパスワードのアカウントを設定している。

$ cat << 'EOF' | sudo tee /etc/ppp/chap-secrets > /dev/null
"testuser" * "testpassword" *
EOF

そして、PPPoE サーバのプロセスを起動する。 -I オプションはリクエストを待ち受けるインターフェイスを指定する。 -L はサーバ側のトンネル終端アドレスの指定になる。 -R はクライアント側のトンネル終端アドレスの開始アドレスを指定する。

$ sudo ip netns exec provider \
    pppoe-server \
      -I wan-veth0 \
      -L 192.0.2.1 \
      -R 192.0.2.10

これでサーバが起動した。

クライアントをセットアップする

続いては PPPoE のクライアントをセットアップする。

はじめに、サーバに接続するための設定ファイルを用意する。 先ほどのアカウントや、使用するインターフェイスの情報を入力する。

$ cat << 'EOF' | sudo tee /etc/ppp/peers/example-provider > /dev/null
user "testuser"
password "testpassword"
plugin rp-pppoe.so
gw-veth0
noauth
defaultroute
EOF

あとは pon(1) を使ってコネクションを開始する。 コマンドの引数は、先ほど作成した設定ファイルのファイル名になる。

$ sudo ip netns exec router \
    pon example-provider
Plugin rp-pppoe.so loaded.

コマンドを実行したら plog(1) を使って pppd(8) のログを確認しよう。 ここにはサーバとクライアントのログが一緒になって表示される

$ plog
May  6 18:14:33 jammy pppd[1726]: rcvd [IPCP ConfAck id=0x2 <addr 192.0.2.1>]
May  6 18:14:33 jammy pppd[1726]: Script /etc/ppp/ip-pre-up started (pid 1734)
May  6 18:14:33 jammy pppd[1722]: local  IP address 192.0.2.10
May  6 18:14:33 jammy pppd[1722]: remote IP address 192.0.2.1
May  6 18:14:33 jammy pppd[1726]: Script /etc/ppp/ip-pre-up finished (pid 1734), status = 0x0
May  6 18:14:33 jammy pppd[1726]: local  IP address 192.0.2.1
May  6 18:14:33 jammy pppd[1726]: remote IP address 192.0.2.10
May  6 18:14:33 jammy pppd[1726]: Script /etc/ppp/ip-up started (pid 1743)
May  6 18:14:33 jammy pppd[1726]: Script /etc/ppp/ipv6-up finished (pid 1733), status = 0x0
May  6 18:14:33 jammy pppd[1726]: Script /etc/ppp/ip-up finished (pid 1743), status = 0x0

上手くいけば IPCP (Internet Protocol Control Protocol) で設定されたトンネルの終端アドレスがログに表示されるはず。

router の方で ip address show サブコマンドを実行すると ppp0 インターフェイスが作成されている。 これが PPP のトンネルを表した仮想インターフェイスになる。 また、トンネル終端アドレスとしては 192.0.2.10 が付与されていることが分かる。

$ sudo ip netns exec router ip address show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ppp0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1492 qdisc fq_codel state UNKNOWN group default qlen 3
    link/ppp 
    inet 192.0.2.10 peer 192.0.2.1/32 scope global ppp0
       valid_lft forever preferred_lft forever
    inet6 fe80::218b:d4d:3f74:4dd4 peer fe80::b176:a450:3399:a86f/128 scope link 
       valid_lft forever preferred_lft forever
3: gw-veth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:02 brd ff:ff:ff:ff:ff:ff link-netns provider
    inet6 fe80::200:5eff:fe00:5302/64 scope link 
       valid_lft forever preferred_lft forever

同様に provider 側も確認すると、こちらも ppp0 インターフェイスが作成されて 192.0.2.1 が付与されている。

$ sudo ip netns exec provider ip address show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ppp0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1492 qdisc fq_codel state UNKNOWN group default qlen 3
    link/ppp 
    inet 192.0.2.1 peer 192.0.2.10/32 scope global ppp0
       valid_lft forever preferred_lft forever
    inet6 fe80::b176:a450:3399:a86f peer fe80::218b:d4d:3f74:4dd4/128 scope link 
       valid_lft forever preferred_lft forever
4: wan-veth0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:01 brd ff:ff:ff:ff:ff:ff link-netns router
    inet 203.0.113.254/24 scope global wan-veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::200:5eff:fe00:5301/64 scope link 
       valid_lft forever preferred_lft forever

router の方でルーティングテーブルを確認すると、トンネルインターフェイスにデフォルトルートが向いていることが確認できる。

$ sudo ip netns exec router ip route show
default dev ppp0 scope link 
192.0.2.1 dev ppp0 proto kernel scope link src 192.0.2.10

試しに router から provider の IP アドレスに ping(1) を打ってみよう。

$ sudo ip netns exec router ping -c 3 203.0.113.254
PING 203.0.113.254 (203.0.113.254) 56(84) bytes of data.
64 bytes from 203.0.113.254: icmp_seq=1 ttl=64 time=0.215 ms
64 bytes from 203.0.113.254: icmp_seq=2 ttl=64 time=0.362 ms
64 bytes from 203.0.113.254: icmp_seq=3 ttl=64 time=0.326 ms

--- 203.0.113.254 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2155ms
rtt min/avg/max/mdev = 0.215/0.301/0.362/0.062 ms

ちゃんと疎通があることが確認できる。

また、routergw-veth0 インターフェイスのパケットをキャプチャしてみると、次のように PPPoE ヘッダが確認できる。

$ sudo ip netns exec router tcpdump -tnel -i gw-veth0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on gw-veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 106: PPPoE  [ses 0x1] IP (0x0021), length 86: 192.0.2.10 > 203.0.113.254: ICMP echo request, id 63894, seq 1, length 64
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 106: PPPoE  [ses 0x1] IP (0x0021), length 86: 203.0.113.254 > 192.0.2.10: ICMP echo reply, id 63894, seq 1, length 64
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 106: PPPoE  [ses 0x1] IP (0x0021), length 86: 192.0.2.10 > 203.0.113.254: ICMP echo request, id 63894, seq 2, length 64
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 106: PPPoE  [ses 0x1] IP (0x0021), length 86: 203.0.113.254 > 192.0.2.10: ICMP echo reply, id 63894, seq 2, length 64
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 106: PPPoE  [ses 0x1] IP (0x0021), length 86: 192.0.2.10 > 203.0.113.254: ICMP echo request, id 63894, seq 3, length 64
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 106: PPPoE  [ses 0x1] IP (0x0021), length 86: 203.0.113.254 > 192.0.2.10: ICMP echo reply, id 63894, seq 3, length 64

PPPoE のやり取りをパケットキャプチャしてみる

次は PPPoE のユーザ認証や設定をしている部分もパケットキャプチャしてみよう。

一旦 poff(1) で PPPoE のコネクションを切断する。

$ sudo ip netns exec router poff example-provider

コネクションを切断すると、次のように ppp0 インターフェイスが見えなくなる。

$ sudo ip netns exec router ip address show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: gw-veth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 00:00:5e:00:53:02 brd ff:ff:ff:ff:ff:ff link-netns provider
    inet6 fe80::200:5eff:fe00:5302/64 scope link 
       valid_lft forever preferred_lft forever

別のターミナルを開いて tcpdump(1) を起動する。 このとき PPPoE のフレームだけキャプチャできるようにフィルタを設定する。

$ sudo ip netns exec router tcpdump -tnel -i gw-veth0 -v "pppoed or pppoes"

パケットキャプチャの準備ができたら pon(1) でコネクションを再接続する。

$ sudo ip netns exec router pon example-provider

すると、次のようなやり取りが確認できる。 最初に PADI / PADO/ PADR/ PADS がやり取りされている。 そして、その次に LCP / CHAP でユーザ認証に関する情報がやり取りされる。 最後に IPCP でトンネル終端の IP アドレスに関する情報がやり取りされる。

$ sudo ip netns exec router tcpdump -tnel -i gw-veth0 -v "pppoed or pppoes"
tcpdump: listening on gw-veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
00:00:5e:00:53:02 > ff:ff:ff:ff:ff:ff, ethertype PPPoE D (0x8863), length 32: PPPoE PADI [Service-Name] [Host-Uniq 0xA6070000]
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE D (0x8863), length 65: PPPoE PADO [AC-Name "jammy"] [Service-Name] [AC-Cookie 0xC761CFF6554C622D68F8CB294C902485B2060000] [Host-Uniq 0xA6070000]
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE D (0x8863), length 56: PPPoE PADR [Service-Name] [Host-Uniq 0xA6070000] [AC-Cookie 0xC761CFF6554C622D68F8CB294C902485B2060000]
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE D (0x8863), length 32: PPPoE PADS [ses 0x2] [Service-Name] [Host-Uniq 0xA6070000]
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 36: PPPoE  [ses 0x2] LCP (0xc021), length 16: LCP, Conf-Request (0x01), id 1, length 16
    encoded length 14 (=Option(s) length 10)
      MRU Option (0x01), length 4: 1492
      Magic-Num Option (0x05), length 6: 0x9ef0d929
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 41: PPPoE  [ses 0x2] LCP (0xc021), length 21: LCP, Conf-Request (0x01), id 1, length 21
    encoded length 19 (=Option(s) length 15)
      MRU Option (0x01), length 4: 1492
      Auth-Prot Option (0x03), length 5: CHAP, MD5
      Magic-Num Option (0x05), length 6: 0xe77abbef
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 41: PPPoE  [ses 0x2] LCP (0xc021), length 21: LCP, Conf-Ack (0x02), id 1, length 21
    encoded length 19 (=Option(s) length 15)
      MRU Option (0x01), length 4: 1492
      Auth-Prot Option (0x03), length 5: CHAP, MD5
      Magic-Num Option (0x05), length 6: 0xe77abbef
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 36: PPPoE  [ses 0x2] LCP (0xc021), length 16: LCP, Conf-Request (0x01), id 1, length 16
    encoded length 14 (=Option(s) length 10)
      MRU Option (0x01), length 4: 1492
      Magic-Num Option (0x05), length 6: 0x9ef0d929
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 36: PPPoE  [ses 0x2] LCP (0xc021), length 16: LCP, Conf-Ack (0x02), id 1, length 16
    encoded length 14 (=Option(s) length 10)
      MRU Option (0x01), length 4: 1492
      Magic-Num Option (0x05), length 6: 0x9ef0d929
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 30: PPPoE  [ses 0x2] LCP (0xc021), length 10: LCP, Echo-Request (0x09), id 0, length 10
    encoded length 8 (=Option(s) length 4)
      Magic-Num 0x9ef0d929
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 30: PPPoE  [ses 0x2] LCP (0xc021), length 10: LCP, Echo-Request (0x09), id 0, length 10
    encoded length 8 (=Option(s) length 4)
      Magic-Num 0xe77abbef
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 30: PPPoE  [ses 0x2] LCP (0xc021), length 10: LCP, Echo-Reply (0x0a), id 0, length 10
    encoded length 8 (=Option(s) length 4)
      Magic-Num 0x9ef0d929
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 53: PPPoE  [ses 0x2] CHAP (0xc223), length 33: CHAP, Challenge (0x01), id 156, Value e7c4ffa78bd4b3c23a430a0062bb63e007de831b81, Name jammy
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 51: PPPoE  [ses 0x2] CHAP (0xc223), length 31: CHAP, Response (0x02), id 156, Value 7fb0ad18e44ec011ffa05894274c33b6, Name testuser
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 30: PPPoE  [ses 0x2] LCP (0xc021), length 10: LCP, Echo-Reply (0x0a), id 0, length 10
    encoded length 8 (=Option(s) length 4)
      Magic-Num 0xe77abbef
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 40: PPPoE  [ses 0x2] CHAP (0xc223), length 20: CHAP, Success (0x03), id 156, Msg Access granted
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 32: PPPoE  [ses 0x2] IPCP (0x8021), length 12: IPCP, Conf-Request (0x01), id 1, length 12
    encoded length 10 (=Option(s) length 6)
      IP-Addr Option (0x03), length 6: 0.0.0.0
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 36: PPPoE  [ses 0x2] IP6CP (0x8057), length 16: IP6CP, Conf-Request (0x01), id 1, length 16
    encoded length 14 (=Option(s) length 10)
      Interface-ID Option (0x01), length 10: 9cb8:5614:c486:dc46
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 37: PPPoE  [ses 0x2] CCP (0x80fd), length 17: CCP, Conf-Request (0x01), id 1, length 17
    encoded length 15 (=Option(s) length 11)
      Deflate Option (0x1a), length 4: Window: 7K, Method: zlib (0x8), MBZ: 0, CHK: 0
      MVRCA Option (0x18), length 4: Features: 1, PxP: Enabled, History: 24, #CTX-ID: 0
      BSD-Comp Option (0x15), length 3: Version: 1, Dictionary Bits: 15
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 26: PPPoE  [ses 0x2] CCP (0x80fd), length 6: CCP, Conf-Request (0x01), id 1, length 6
    encoded length 4 (=Option(s) length 0)
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 37: PPPoE  [ses 0x2] CCP (0x80fd), length 17: CCP, Conf-Reject (0x04), id 1, length 17
    encoded length 15 (=Option(s) length 11)
      Deflate Option (0x1a), length 4: Window: 7K, Method: zlib (0x8), MBZ: 0, CHK: 0
      MVRCA Option (0x18), length 4: Features: 1, PxP: Enabled, History: 24, #CTX-ID: 0
      BSD-Comp Option (0x15), length 3: Version: 1, Dictionary Bits: 15
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 38: PPPoE  [ses 0x2] IPCP (0x8021), length 18: IPCP, Conf-Request (0x01), id 1, length 18
    encoded length 16 (=Option(s) length 12)
      IP-Comp Option (0x02), length 6: VJ-Comp (0x2d):
      IP-Addr Option (0x03), length 6: 192.0.2.1
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 32: PPPoE  [ses 0x2] IPCP (0x8021), length 12: IPCP, Conf-Reject (0x04), id 1, length 12
    encoded length 10 (=Option(s) length 6)
      IP-Comp Option (0x02), length 6: VJ-Comp (0x2d):
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 36: PPPoE  [ses 0x2] IP6CP (0x8057), length 16: IP6CP, Conf-Request (0x01), id 1, length 16
    encoded length 14 (=Option(s) length 10)
      Interface-ID Option (0x01), length 10: 54d8:292a:6f6e:60fd
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 36: PPPoE  [ses 0x2] IP6CP (0x8057), length 16: IP6CP, Conf-Ack (0x02), id 1, length 16
    encoded length 14 (=Option(s) length 10)
      Interface-ID Option (0x01), length 10: 54d8:292a:6f6e:60fd
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 32: PPPoE  [ses 0x2] IPCP (0x8021), length 12: IPCP, Conf-Nack (0x03), id 1, length 12
    encoded length 10 (=Option(s) length 6)
      IP-Addr Option (0x03), length 6: 192.0.2.11
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 32: PPPoE  [ses 0x2] IPCP (0x8021), length 12: IPCP, Conf-Request (0x01), id 2, length 12
    encoded length 10 (=Option(s) length 6)
      IP-Addr Option (0x03), length 6: 192.0.2.11
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 36: PPPoE  [ses 0x2] IP6CP (0x8057), length 16: IP6CP, Conf-Ack (0x02), id 1, length 16
    encoded length 14 (=Option(s) length 10)
      Interface-ID Option (0x01), length 10: 9cb8:5614:c486:dc46
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 70: PPPoE  [ses 0x2] IP6 (0x0057), length 50: (hlim 255, next-header ICMPv6 (58) payload length: 8) fe80::9cb8:5614:c486:dc46 > ff02::2: [icmp6 sum ok] ICMP6, router solicitation, length 8
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 26: PPPoE  [ses 0x2] CCP (0x80fd), length 6: CCP, Conf-Ack (0x02), id 1, length 6
    encoded length 4 (=Option(s) length 0)
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 26: PPPoE  [ses 0x2] CCP (0x80fd), length 6: CCP, Conf-Request (0x01), id 2, length 6
    encoded length 4 (=Option(s) length 0)
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 32: PPPoE  [ses 0x2] IPCP (0x8021), length 12: IPCP, Conf-Request (0x01), id 2, length 12
    encoded length 10 (=Option(s) length 6)
      IP-Addr Option (0x03), length 6: 192.0.2.1
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 70: PPPoE  [ses 0x2] IP6 (0x0057), length 50: (hlim 255, next-header ICMPv6 (58) payload length: 8) fe80::54d8:292a:6f6e:60fd > ff02::2: [icmp6 sum ok] ICMP6, router solicitation, length 8
00:00:5e:00:53:01 > 00:00:5e:00:53:02, ethertype PPPoE S (0x8864), length 32: PPPoE  [ses 0x2] IPCP (0x8021), length 12: IPCP, Conf-Ack (0x02), id 2, length 12
    encoded length 10 (=Option(s) length 6)
      IP-Addr Option (0x03), length 6: 192.0.2.11
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 26: PPPoE  [ses 0x2] CCP (0x80fd), length 6: CCP, Conf-Ack (0x02), id 2, length 6
    encoded length 4 (=Option(s) length 0)
00:00:5e:00:53:02 > 00:00:5e:00:53:01, ethertype PPPoE S (0x8864), length 32: PPPoE  [ses 0x2] IPCP (0x8021), length 12: IPCP, Conf-Ack (0x02), id 2, length 12
    encoded length 10 (=Option(s) length 6)
      IP-Addr Option (0x03), length 6: 192.0.2.1

なお、今回はトンネル終端アドレスしか配布していないが DNS サーバなどの情報も配布できる。

まとめ

今回は Linux の Network Namespace の環境で PPPoE のサーバとクライアントを動作させてやり取りされるフレームを観察した。

Python: scikit-learn の LabelEncoder を説明変数の変換に使うのは誤り

scikit-learn の LabelEncoder を説明変数の変換に使っている例はたくさん見つかる。 しかし、実は本来 LabelEncoder は目的変数の変換に使うことが想定されていることは、あまり知られていない。 これは公式のドキュメントで確認できる。

scikit-learn.org

上記から一部を引用する。

This transformer should be used to encode target values, i.e. y, and not the input X.

このように、入力として想定されているのが本来は目的変数であることが読み取れる。 ようするに Iris データセットでいう setosa とか versicolor を 0 とか 1 に変換するのが本来の目的ということ。

メソッドの引数名を見ても X ではなく y になっている。 なので Pipeline と併用した場合には X の内容は渡ってこない。

scikit-learn.org

説明変数の変換には、代わりに OrdinalEncoder を使うのが良い。

scikit-learn.org

OrdinalEncoder を使うと、未知の値がテストデータにあったときに指定した値に変換できる。 この機能は LabelEncoder には含まれていない。 ただし、デフォルトでは LabelEncoder と同じように未知の値に遭遇したときは例外を上げるようになっている。 そのため、明示的に handle_unknown パラメータと unknown_value を指定する。 サンプルコードを以下に示す。

import numpy as np
from sklearn.preprocessing import OrdinalEncoder


def main():
    # 説明変数を仮定しているため 2D 配列の必要がある
    train = np.array(["apple", "banana", "cherry"]).reshape(-1, 1)

    # 未知の値は -1 に変換する
    # デフォルトでは例外になるので LabelEncoder と変わらない
    ordinal_encoder = OrdinalEncoder(
        handle_unknown="use_encoded_value",
        unknown_value=-1,
    )
    ordinal_encoder.fit(train)

    train_transformed = ordinal_encoder.transform(train)
    print(train_transformed)

    # 未知の値は -1 に変換される
    test = np.array(["dates", "fig"]).reshape(-1, 1)
    test_transformed = ordinal_encoder.transform(test)
    print(test_transformed)


if __name__ == "__main__":
    main()

上記を実行してみよう。

$ python example.py
[[0.]
 [1.]
 [2.]]
[[-1.]
 [-1.]]

未知のデータが含まれた配列が例外にならず -1 に変換できていることが確認できる。

いじょう。

strongSwan の IPsec VPN を Network Namespace で試す (Route-based / VTI デバイス)

strongSwan は IPsec VPN を構成するのに用いられるソフトウェア。 今回は、その strongSwan を Network Namespace で作ったネットワーク上で動かしてみる。 動作モードとしては VTI (Virtual Tunnel Interface) デバイスを使った Route-based を利用する。

なお、現行バージョンの strongSwan は設定ファイルや PID ファイルの置き場所がビルド時にバイナリへハードコードされる。 そのため、そのままでは単一のホスト上で複数のインスタンスを動作させることができない。 この課題については Mount Namespace を併用することで克服した。

使った環境は次のとおり

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.2 LTS"
$ uname -srm
Linux 5.15.0-69-generic x86_64
$ ipsec version
Linux strongSwan U5.9.5/K5.15.0-69-generic
University of Applied Sciences Rapperswil, Switzerland
See 'ipsec --copyright' for copyright information.

もくじ

下準備

まずは strongSwan をインストールする。

$ sudo apt-get install strongswan

インストールしたタイミングで systemd の ipsec サービスが動作し始める。 今回は systemd のサービスは不要なので止めておく。

$ sudo systemctl stop ipsec
$ sudo systemctl disable ipsec

ネットワークを作る

まずは Network Namespace を使ってネットワークを作っていく。 この工程では IPsec VPN とかは関係ない。 ただ、単一のセグメント (203.0.113.0/24) のネットワークを構成するだけ。

はじめに、IPsec VPN のトンネルを張るルータに相当するネームスペースを用意する。

$ sudo ip netns add router1
$ sudo ip netns add router2

両者を接続するための Virtual Ethernet デバイスのインターフェイスを作成する。

$ sudo ip link add rt1-veth0 type veth peer name rt2-veth0

両端のインターフェイスをネームスペースに所属させる。

$ sudo ip link set rt1-veth0 netns router1
$ sudo ip link set rt2-veth0 netns router2

インターフェイスをリンクアップさせる。

$ sudo ip netns exec router1 ip link set rt1-veth0 up
$ sudo ip netns exec router2 ip link set rt2-veth0 up

インターフェイスに IPv4 アドレスを付与する。

$ sudo ip netns exec router1 ip address add 203.0.113.1/24 dev rt1-veth0
$ sudo ip netns exec router2 ip address add 203.0.113.2/24 dev rt2-veth0

ルータとして動作するように、それぞれのネームスペースについてカーネルのパラメータを変更する。 ただ、今回の構成であればおそらく設定しなくても問題ない。

$ sudo ip netns exec router1 sysctl net.ipv4.ip_forward=1
$ sudo ip netns exec router2 sysctl net.ipv4.ip_forward=1

これでネットワークはできた。 一旦 L3 で疎通があることを ping で確認しておく。

$ sudo ip netns exec router1 ping -c 3 203.0.113.2 -I 203.0.113.1
PING 203.0.113.2 (203.0.113.2) from 203.0.113.1 : 56(84) bytes of data.
64 bytes from 203.0.113.2: icmp_seq=1 ttl=64 time=0.086 ms
64 bytes from 203.0.113.2: icmp_seq=2 ttl=64 time=0.071 ms
64 bytes from 203.0.113.2: icmp_seq=3 ttl=64 time=0.070 ms

--- 203.0.113.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2026ms
rtt min/avg/max/mdev = 0.070/0.075/0.086/0.007 ms

IPsec VPN を構成する

続いて IPsec VPN に関する設定をしていく。

まず、デフォルトで /etc 以下に用意されている strongSwan の設定ファイルを一旦 /var/tmp/strongswan-conf にコピーしておく。 これは、前述したとおり strongSwan のバイナリに設定ファイルの場所がハードコードされているため。 複数のインスタンスを動かすには一旦別の場所に退避させておく必要がある。

$ mkdir -p /var/tmp/strongswan-conf
$ cp /etc/strongswan.conf /var/tmp/strongswan-conf/
$ cp -r /etc/strongswan.d/ /var/tmp/strongswan-conf/

コピーした設定ファイルの中で charon.conf を修正する。 設定ファイルの中の install_routes というキーを no に設定する。 この修正は strongSwan を Route-based で動作させる際に必要になる。

$ sed -i.back -e "s/# install_routes = yes/install_routes = no/" /var/tmp/strongswan-conf/strongswan.d/charon.conf

設定項目がちゃんと書き換わっていることを確認する。

$ grep install_routes /var/tmp/strongswan-conf/strongswan.d/charon.conf
    install_routes = no

router1 を設定する

ここからは Network Namespace で作成した各ルータをシェルで操作していく。 以後は、どのルータを操作しているか分かりやすいように、左端に名前を補足としてつけておく。

まずは router1 から。 新しくシェルを起動したら unshare(1) で Mount Namespace を有効にする。 こうすることで、マウントの操作をしてもシステム側に影響を与えなくする。

(router1) $ sudo unshare --mount bash

さらに nsenter(1) を使って、先ほど作成した Network Namespace を有効にする。

(router1) # nsenter --net=/var/run/netns/router1

次のように rt1-veth0 インターフェイスが見えるようになる。

(router1) # ip address show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: rt1-veth0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 02:74:2c:c1:b3:26 brd ff:ff:ff:ff:ff:ff link-netns router2
    inet 203.0.113.1/24 scope global rt1-veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::74:2cff:fec1:b326/64 scope link 
       valid_lft forever preferred_lft forever

続いて /etc/var/run に tmpfs で作ったファイルシステムをマウントする。 これら 2 つのディレクトリは、前述したとおり Ubuntu の strongSwan バイナリにハードコードされた設定ファイルと PID ファイルの置き場所になっている。 Mount Namespace を分けてあるので、ここでマウントの操作をしてもシステム側に影響が及ぶことはない。

(router1) # mount -t tmpfs tmpfs /etc/
(router1) # mount -t tmpfs tmpfs /var/run

マウントしたディレクトリが空っぽに見えることを確認する。

(router1) # ls /etc
(router1) # ls /var/run

空っぽになった /etc 以下に設定ファイルを配置していく。 /etc/ipsec.conf に IKE や IPsec の動作パラメータを記述していく。 ここでは 203.0.113.1203.0.113.2 の間で IPsec VPN を張る。 トンネル両端のサブネットには 192.0.2.0/24198.51.100.0/24 を使う。

(router1) # cat << 'EOF' > /etc/ipsec.conf
config setup
    charondebug="all"
conn %default
    authby=psk
    type=tunnel
    auto=start
conn myvpn
    keyexchange=ikev2
    left=203.0.113.1
    leftsubnet=192.0.2.0/24
    right=203.0.113.2
    rightsubnet=198.51.100.0/24
    ike=aes256-sha256-modp1024!
    esp=aes256!
    mark=42
EOF

事前共有鍵を /etc/ipsec.secrets に書き込む。 もちろん、本来であればもっとちゃんとした事前共有鍵を使った方が良い。

(router1) # cat << 'EOF' > /etc/ipsec.secrets 
203.0.113.1 203.0.113.2 : PSK "DeadBeef"
EOF

先ほど /etc 以下から退避させておいたコンフィグ群を書き戻す。

(router1) # cp /var/tmp/strongswan-conf/strongswan.conf /etc/
(router1) # cp -r /var/tmp/strongswan-conf/strongswan.d/ /etc/

この時点で /etc 以下は次のような構成になっている。

(router1) # ls /etc
ipsec.conf  ipsec.secrets  strongswan.conf  strongswan.d

あとは ipsec start コマンドを実行すると関連するデーモンが動き始める。

(router1) # ipsec start --debug
Starting strongSwan 5.9.5 IPsec [starter]...
Loading config setup
Loading conn 'myvpn'

このとき PID ファイルが /var/run 以下に作られる。

なお、プロセスをフォアグラウンドで動作させたいときは、次のように --nofork オプションを付けておくと良い。

(router1) # ipsec start --nofork --debug

router2 を設定する

router2 についても、同様に設定していく。 やることはほとんど変わらない。

Mount Namespace と Network Namespace を有効にしたシェルを新たに用意する。

(router2) $ sudo unshare --mount bash
(router2) # nsenter --net=/var/run/netns/router2
(router2) # ip address show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: rt2-veth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 62:8a:44:2c:56:0a brd ff:ff:ff:ff:ff:ff link-netns router1
    inet 203.0.113.2/24 scope global rt2-veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::608a:44ff:fe2c:560a/64 scope link 
       valid_lft forever preferred_lft forever

設定ファイルを書き込む。

(router2) # mount -t tmpfs tmpfs /etc/
(router2) # mount -t tmpfs tmpfs /var/run
(router2) # cat << 'EOF' > /etc/ipsec.conf
config setup
    charondebug="all"
conn %default
    authby=psk
    type=tunnel
    auto=start
conn myvpn
    keyexchange=ikev2
    ike=aes256-sha256-modp1024!
    esp=aes256!
    left=203.0.113.2
    leftsubnet=198.51.100.0/24
    right=203.0.113.1
    rightsubnet=192.0.2.0/24
    mark=42
EOF
(router2) # cat << 'EOF' > /etc/ipsec.secrets 
203.0.113.2 203.0.113.1 : PSK "DeadBeef"
EOF
(router2) # cp /var/tmp/strongswan-conf/strongswan.conf /etc/
(router2) # cp -r /var/tmp/strongswan-conf/strongswan.d/ /etc/
(router2) # ls /etc
ipsec.conf  ipsec.secrets  strongswan.conf  strongswan.d

ipsec start でデーモンを起動する。

(router2) # ipsec start --debug
Starting strongSwan 5.9.5 IPsec [starter]...
Loading config setup
Loading conn 'myvpn'

このとき Mount Namespace が分かれていないと PID ファイルが既にあると言われて起動に失敗する。

SA (Security Association) の状態を確認する

ここまで作業が終わったら IKE / IPsec SA の状態を確認する。 ipsec status コマンドを実行して、次のように SA が確立していれば問題ない。

(router1) # ipsec status
Security Associations (1 up, 0 connecting):
       myvpn[1]: ESTABLISHED 4 seconds ago, 203.0.113.1[203.0.113.1]...203.0.113.2[203.0.113.2]
       myvpn{1}:  INSTALLED, TUNNEL, reqid 1, ESP SPIs: c31aaf62_i c5314c6e_o
       myvpn{1}:   192.0.2.0/24 === 198.51.100.0/24

router2 についても同様に SA が確立していることを確認しておく。

(router2) # ipsec status
Security Associations (1 up, 0 connecting):
       myvpn[2]: ESTABLISHED 8 seconds ago, 203.0.113.2[203.0.113.2]...203.0.113.1[203.0.113.1]
       myvpn{2}:  INSTALLED, TUNNEL, reqid 1, ESP SPIs: c5314c6e_i c31aaf62_o
       myvpn{2}:   198.51.100.0/24 === 192.0.2.0/24

これで IPsec VPN のトンネルはできた。

VTI デバイスを追加して経路を設定する

続いては VTI デバイスを追加して経路を設定する。

一般に、ここからの作業はシェルスクリプトにした上でデーモンからキックしてもらうことで自動化することが多い 1。 しかし、今回は分かりやすさのために手作業でこなしていく。

まずは router1 の方に VTI デバイスのインターフェイスを追加する。 ここで local と remote には、先ほど構築した IPsec VPN のトンネルのエンドポイントになっているアドレスを指定する。 そして key には /etc/ipsec.conf に指定した mark と同じ値を指定する。 こうすることで処理の対象にする IPsec SA を識別するようだ。

(router1) # ip tunnel add ipsec0 mode vti local 203.0.113.1 remote 203.0.113.2 key 42

インターフェイスをリンクアップさせる。

(router1) # ip link set ipsec0 up

動作確認用としてインターフェイスに IPv4 アドレスを設定する。 このとき、アドレスは /etc/ipsec.confleftsubnet に指定したサブネットに属するものにする。

(router1) # ip addr add 192.0.2.1/24 dev ipsec0

/etc/ipsec.confrightsubnet に到達するには、追加したインターフェイスを経由するように経路を追加する。

(router1) # ip route add 198.51.100.0/24 dev ipsec0

同じ内容を router2 側にも設定する。

(router2) # ip tunnel add ipsec0 mode vti local 203.0.113.2 remote 203.0.113.1 key 42
(router2) # ip link set ipsec0 up
(router2) # ip addr add 198.51.100.1/24 dev ipsec0
(router2) # ip route add 192.0.2.0/24 dev ipsec0

動作を確認する

これで全ての設定が完了した。 試しに VTI デバイスのインターフェイスの間で ping を打ってみよう。

$ sudo ip netns exec router1 ping 198.51.100.1 -I 192.0.2.1
PING 198.51.100.1 (198.51.100.1) from 192.0.2.1 : 56(84) bytes of data.
64 bytes from 198.51.100.1: icmp_seq=1 ttl=64 time=0.196 ms
64 bytes from 198.51.100.1: icmp_seq=2 ttl=64 time=0.170 ms
64 bytes from 198.51.100.1: icmp_seq=3 ttl=64 time=0.190 ms
...

どうやらちゃんと疎通があるようだ。

インターフェイスをパケットキャプチャすると、ちゃんとパケットがやり取りされている。 当たり前だけど、ここの内容は平文になっている。

$ sudo ip netns exec router1 tcpdump -tnl -i ipsec0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on ipsec0, link-type RAW (Raw IP), snapshot length 262144 bytes
IP 192.0.2.1 > 198.51.100.1: ICMP echo request, id 8611, seq 75, length 64
IP 198.51.100.1 > 192.0.2.1: ICMP echo reply, id 8611, seq 75, length 64
IP 192.0.2.1 > 198.51.100.1: ICMP echo request, id 8611, seq 76, length 64
IP 198.51.100.1 > 192.0.2.1: ICMP echo reply, id 8611, seq 76, length 64
IP 192.0.2.1 > 198.51.100.1: ICMP echo request, id 8611, seq 77, length 64
IP 198.51.100.1 > 192.0.2.1: ICMP echo reply, id 8611, seq 77, length 64
...

一方で Virtual Ethernet デバイスのインターフェイスの方をキャプチャすると IPsec の ESP パケットの形で流れている。

$ sudo ip netns exec router1 tcpdump -tnl -i rt1-veth0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on rt1-veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP 203.0.113.1 > 203.0.113.2: ESP(spi=0xc5314c6e,seq=0x18), length 120
IP 203.0.113.2 > 203.0.113.1: ESP(spi=0xc31aaf62,seq=0x18), length 120
IP 203.0.113.1 > 203.0.113.2: ESP(spi=0xc5314c6e,seq=0x19), length 120
IP 203.0.113.2 > 203.0.113.1: ESP(spi=0xc31aaf62,seq=0x19), length 120
IP 203.0.113.1 > 203.0.113.2: ESP(spi=0xc5314c6e,seq=0x1a), length 120
IP 203.0.113.2 > 203.0.113.1: ESP(spi=0xc31aaf62,seq=0x1a), length 120
...

IKE のネゴシエーションの様子も確認しておこう。 次のように ESP 以外に IKE 関連のパケットもキャプチャできるようにして tcpdump を起動する。

$ sudo ip netns exec router1 tcpdump -tnl -i rt1-veth0 esp or udp port 500 or udp port 4500 or tcp port 4500
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on rt1-veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes

そして、どちらでも良いので router[12] で ipsec stop してから ipsec start する。 ようするに IKE のネゴシエーションからやり直してもらう。

(router2) # ipsec stop
(router2) # ipsec start

すると、次のように IKE 関連のパケットがキャプチャできる。

$ sudo ip netns exec router1 tcpdump -tnl -i rt1-veth0 esp or udp port 500 or udp port 4500 or tcp port 4500
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on rt1-veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP 203.0.113.2.500 > 203.0.113.1.500: isakmp: parent_sa ikev2_init[I]
IP 203.0.113.1.500 > 203.0.113.2.500: isakmp: parent_sa ikev2_init[R]
IP 203.0.113.2.4500 > 203.0.113.1.4500: NONESP-encap: isakmp: child_sa  ikev2_auth[I]
IP 203.0.113.1.4500 > 203.0.113.2.4500: NONESP-encap: isakmp: child_sa  ikev2_auth[R]

パケットについている [I][R] は Initiator と Responder のどちらなのかを表しているようだ。

まとめ

今回は strongSwan を Network Namespace と Mount Namespace を駆使することで単一ホスト上で動かしてみた。

WireGuard の VPN を Linux の Network Namespace で試す

WireGuard は VPN を構成するための一連の実装と通信プロトコル。 実装のコードベースが小さく、他の VPN ソフトウェアと比べて設定方法がシンプルという特徴がある。 今回は、その WireGuard を Linux の Network Namespace と一緒に試してみる。

使った環境は次のとおり。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.2 LTS"
$ uname -srm
Linux 5.15.0-69-generic x86_64

もくじ

下準備

あらかじめ必要なパッケージをインストールしておく。 WireGuard に直接関係するのは wireguard-tools だけ。

$ sudo apt-get install wireguard-tools iproute2 tcpdump

WireGuard のカーネルモジュールがロードされていることを確認しておく。

$ lsmod | grep -i wireguard
wireguard              94208  0
curve25519_x86_64      36864  1 wireguard
libchacha20poly1305    16384  1 wireguard
ip6_udp_tunnel         16384  1 wireguard
udp_tunnel             20480  1 wireguard
libcurve25519_generic    49152  2 curve25519_x86_64,wireguard

もしロードされていなければ modprobe する。

$ sudo modprobe wireguard

ネットワークを用意する

まずは Network Namespace を使ってネットワークを用意する。 なお、この工程ではまだ VPN とか暗号化は関係がない。 作るネットワークはセグメントが 1 つしかないシンプルなもの。 そこにホストとして Network Namespace が 2 つ繋がる。

はじめに peerapeerb という名前で 2 つの Network Namespace を作る。

$ sudo ip netns add peera
$ sudo ip netns add peerb

2 つの Network Namespace の間を結線する Virtual Ethernet デバイスのインターフェイスを用意する。

$ sudo ip link add peera-veth0 type veth peer name peerb-veth0

インターフェイスの両端を Network Namespace に移動してリンクアップさせる。

$ sudo ip link set peera-veth0 netns peera
$ sudo ip link set peerb-veth0 netns peerb
$ sudo ip netns exec peera ip link set peera-veth0 up
$ sudo ip netns exec peerb ip link set peerb-veth0 up

インターフェイスに IPv4 アドレスを付与しておく。 一応 MAC アドレスも変更しているが必須ではない。

$ sudo ip netns exec peera ip address add 192.0.2.1/24 dev peera-veth0
$ sudo ip netns exec peerb ip address add 192.0.2.2/24 dev peerb-veth0
$ sudo ip netns exec peera ip link set dev peera-veth0 address 00:00:5E:00:53:01
$ sudo ip netns exec peerb ip link set dev peerb-veth0 address 00:00:5E:00:53:02

これで Network Namespace 同士が L3 で疎通した。

$ sudo ip netns exec peera ping -c 3 192.0.2.2 -I 192.0.2.1
PING 192.0.2.2 (192.0.2.2) from 192.0.2.1 : 56(84) bytes of data.
64 bytes from 192.0.2.2: icmp_seq=1 ttl=64 time=0.079 ms
64 bytes from 192.0.2.2: icmp_seq=2 ttl=64 time=0.058 ms
64 bytes from 192.0.2.2: icmp_seq=3 ttl=64 time=0.057 ms

--- 192.0.2.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2041ms
rtt min/avg/max/mdev = 0.057/0.064/0.079/0.010 ms

WireGuard で VPN を構成する

ここからは、作成したネットワーク上に WireGuard で VPN を構成していく。

WireGuard が動作するには、各ノードごとに公開鍵ペアが必要になる。 まずは wg genkey コマンドを使って秘密鍵を生成する。

$ umask 077
$ wg genkey > peer-a-privatekey
$ wg genkey > peer-b-privatekey

次に wg pubkey コマンドを使って秘密鍵から公開鍵を生成する。

$ wg pubkey < peer-a-privatekey > peer-a-publickey
$ wg pubkey < peer-b-privatekey > peer-b-publickey

生成された鍵はいずれもテキストファイルになっている。 たとえば公開鍵ならこんな感じ。

$ cat peer-a-publickey 
FSofL/Slt+l1ga1hLaOq4yd4tJgNDWXX2dF1Q8Z4uRs=
$ cat peer-b-publickey
uhLVCdXY/dpVjYMCLAVHg5ye/afRrN5exDsFjBGO3C8=

公開鍵ペアができたら、次に WireGuard デバイスのインターフェイスを設定していく。 まずは Network Namespace の peerawg0 という名前で WireGuard インターフェイスを追加する。

$ sudo ip netns exec peera ip link add dev wg0 type wireguard

追加した WireGuard インターフェイスに IPv4 アドレスを付与する。 これが VPN トンネルの端点のアドレスになる。

$ sudo ip netns exec peera ip addr add 198.51.100.1/24 dev wg0

次に wg set コマンドを使って WireGuard インターフェイスに VPN 通信の受信用ポートと秘密鍵が格納されたファイルを設定する。 使うポートには特に決まりがないようだ。

$ sudo ip netns exec peera wg set wg0 listen-port 37564 private-key ./peer-a-privatekey

続いて VPN トンネルを張るピアに関する情報を設定する。 ここではピアの公開鍵とエンドポイント、送信元になる端点のアドレスなどを指定する。

$ sudo ip netns exec peera wg set wg0 peer $(cat peer-b-publickey) persistent-keepalive 25 allowed-ips 198.51.100.2/32 endpoint 192.0.2.2:37564

あとは WireGuard インターフェイスをリンクアップさせる。

$ sudo ip netns exec peera ip link set wg0 up

wg コマンドで設定されている内容を確認できる。

$ sudo ip netns exec peera wg
interface: wg0
  public key: FSofL/Slt+l1ga1hLaOq4yd4tJgNDWXX2dF1Q8Z4uRs=
  private key: (hidden)
  listening port: 37564

peer: uhLVCdXY/dpVjYMCLAVHg5ye/afRrN5exDsFjBGO3C8=
  endpoint: 192.0.2.2:37564
  allowed ips: 198.51.100.2/32
  latest handshake: 25 seconds ago
  transfer: 564 B received, 688 B sent
  persistent keepalive: every 25 seconds

peera と対になる設定を peerb にも設定する。

$ sudo ip netns exec peerb ip link add dev wg0 type wireguard
$ sudo ip netns exec peerb ip addr add 198.51.100.2/24 dev wg0
$ sudo ip netns exec peerb wg set wg0 listen-port 37564 private-key ./peer-b-privatekey
$ sudo ip netns exec peerb wg set wg0 peer $(cat peer-a-publickey) persistent-keepalive 25 allowed-ips 198.51.100.1/32 endpoint 192.0.2.1:37564
$ sudo ip netns exec peerb ip link set wg0 up
$ sudo ip netns exec peerb wg
interface: wg0
  public key: uhLVCdXY/dpVjYMCLAVHg5ye/afRrN5exDsFjBGO3C8=
  private key: (hidden)
  listening port: 37564

peer: FSofL/Slt+l1ga1hLaOq4yd4tJgNDWXX2dF1Q8Z4uRs=
  endpoint: 192.0.2.1:37564
  allowed ips: 198.51.100.1/32
  latest handshake: 49 seconds ago
  transfer: 572 B received, 596 B sent
  persistent keepalive: every 25 seconds

これで VPN トンネルが張られた。

動作を確認する

それでは、実際にトンネルの端点間で ping を打ってみよう。

$ sudo ip netns exec peera ping -c 3 198.51.100.2 -I 198.51.100.1
PING 198.51.100.2 (198.51.100.2) from 198.51.100.1 : 56(84) bytes of data.
64 bytes from 198.51.100.2: icmp_seq=1 ttl=64 time=0.371 ms
64 bytes from 198.51.100.2: icmp_seq=2 ttl=64 time=0.740 ms
64 bytes from 198.51.100.2: icmp_seq=3 ttl=64 time=1.09 ms

--- 198.51.100.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2007ms
rtt min/avg/max/mdev = 0.371/0.732/1.086/0.291 ms

ちゃんと疎通があるようだ。

パケットキャプチャもしてみよう。 ping を打ちっぱなしにした状態で peerbwg0 インターフェイスをキャプチャする。 すると、次のように平文の ICMP パケットが流れている。

$ sudo ip netns exec peerb tcpdump -tnl -i wg0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wg0, link-type RAW (Raw IP), snapshot length 262144 bytes
IP 198.51.100.1 > 198.51.100.2: ICMP echo request, id 39464, seq 1, length 64
IP 198.51.100.2 > 198.51.100.1: ICMP echo reply, id 39464, seq 1, length 64
IP 198.51.100.1 > 198.51.100.2: ICMP echo request, id 39464, seq 2, length 64
IP 198.51.100.2 > 198.51.100.1: ICMP echo reply, id 39464, seq 2, length 64
IP 198.51.100.1 > 198.51.100.2: ICMP echo request, id 39464, seq 3, length 64
IP 198.51.100.2 > 198.51.100.1: ICMP echo reply, id 39464, seq 3, length 64

一方で peerb-veth0 インターフェイスをキャプチャすると UDP:37564 ポートで通信がやり取りされている。 これは、WireGuard がトランスポート層のプロトコルに UDP を使うため。 つまり、これが VPN でやり取りされる暗号化されたパケットということになる。

$ sudo ip netns exec peerb tcpdump -tnl -i peerb-veth0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on peerb-veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP 192.0.2.1.37564 > 192.0.2.2.37564: UDP, length 128
IP 192.0.2.2.37564 > 192.0.2.1.37564: UDP, length 128
IP 192.0.2.2.37564 > 192.0.2.1.37564: UDP, length 148
IP 192.0.2.1.37564 > 192.0.2.2.37564: UDP, length 92
IP 192.0.2.2.37564 > 192.0.2.1.37564: UDP, length 32
IP 192.0.2.1.37564 > 192.0.2.2.37564: UDP, length 128
IP 192.0.2.2.37564 > 192.0.2.1.37564: UDP, length 128
IP 192.0.2.1.37564 > 192.0.2.2.37564: UDP, length 128
IP 192.0.2.2.37564 > 192.0.2.1.37564: UDP, length 128

いじょう。

Python: Pandas 2 系ではデータ型のバックエンドを変更できる

Pandas の 2 系から、新たにデータ型のバックエンドという考え方が導入された。 これは、端的にいうと DataFrame のデータをどのような形式で持つかを表している。 たとえば Pandas 2.0.0 の時点では、次の 3 つからバックエンドを選ぶことができる。

  • NumPy (デフォルト)
  • NumPy Nullable
  • PyArrow

何も指定しないときに選ばれるデフォルトの NumPy は 1 系と使い勝手が変わらない。 このエントリでは、バックエンドを切り替えたときに何が起こるのかを解説する。 また Pandas の内部実装についても軽く紹介する。

使った環境は次のとおり。

$ sw_vers   
ProductName:        macOS
ProductVersion:     13.3
BuildVersion:       22E252
$ python -V         
Python 3.10.11
$ pip list | grep pandas
pandas          2.0.0

もくじ

下準備

あらかじめ Pandas と PyArrow をインストールしておく。 PyArrow についてはインストールしておかないとバックエンドに指定できない。

$ pip install -U pandas pyarrow

欠損値の扱いについて

分かりやすいところで欠損値の扱いについて紹介する。 以下のサンプルコードでは「fruits」と「price」という 2 つのカラムを持った CSV ファイルを読み込んでいる。 CSV ファイルの読み込みには pandas.read_csv() 関数を使う。 Pandas 2 系では、この関数に dtype_backend という引数が追加された。 この引数に "numpy_nullable" や "pyarrow" を指定することでバックエンドを変更できる。 ちなみに pandas.read_csv() 以外のデータを読み込む関数にも、同様に dtype_backend が追加された。 なお、既存の DataFrame のバックエンドを変更したいときは DataFrame#convert_dtypes() を使えば良い。

import io

import pandas as pd


def main():
    # CSV フォーマットのファイルライクオブジェクトを用意する
    csv_data = """
    fruits,price
    apple,198
    banana,
    cherry,398
    """
    csv_file_obj = io.StringIO(csv_data)

    # デフォルトの NumPy バックエンド
    default_df = pd.read_csv(
        csv_file_obj,
    )
    print("Default NumPy backend")
    print(default_df)
    print(default_df.dtypes)
    print("-" * 60)

    csv_file_obj.seek(0)
    # Nullable NumPy バックエンド
    # 浮動小数点以外でも欠損値を持てる
    nullable_df = pd.read_csv(
        csv_file_obj,
        dtype_backend="numpy_nullable",
    )
    print("Nullable NumPy backend")
    print(nullable_df)
    print(nullable_df.dtypes)
    print("-" * 60)

    csv_file_obj.seek(0)
    # PyArrow バックエンド
    # これも浮動小数点以外で欠損値を持てる
    pyarrow_df = pd.read_csv(
        csv_file_obj,
        dtype_backend="pyarrow",
    )
    print("PyArrow backend")
    print(pyarrow_df)
    print(pyarrow_df.dtypes)
    print("-" * 60)


if __name__ == "__main__":
    main()

「price」には欠損値が含まれるが、バックエンドによってどのような違いが出るだろうか。 上記の実行結果は次のとおり。

$ python backend.py 
Default NumPy backend
       fruits  price
0       apple  198.0
1      banana    NaN
2      cherry  398.0
    fruits     object
price         float64
dtype: object
------------------------------------------------------------
Nullable NumPy backend
       fruits  price
0       apple    198
1      banana   <NA>
2      cherry    398
    fruits    string[python]
price                  Int64
dtype: object
------------------------------------------------------------
PyArrow backend
       fruits  price
0       apple    198
1      banana   <NA>
2      cherry    398
    fruits    string[pyarrow]
price          int64[pyarrow]
dtype: object
------------------------------------------------------------

デフォルトの NumPy バックエンドでは欠損値があることでデータ型が暗黙に浮動小数点型 (float64) にキャストされてしまっている。 一方で NumPy Nullable や PyArrow では整数型 (Int64 / int64[pyarrow]) が維持されている。 このように、データ型のバックエンドを切り替えると振る舞いが変わることがある。

内部実装について

ここからは、データ型のバックエンドがどのように実現されているのか内部実装を見ていこう。 先ほどのサンプルコードで使った変数を Python の REPL で操作できるようにした。

NumPy バックエンド

まずはデフォルトの NumPy バックエンドを使った DataFrame から見ていく。

DataFrame のデータは、内部的に DataFrame#_data という変数に格納されている。 このオブジェクトは BlockManager と呼ばれる。 BlockManager には複数のブロックが登録されており、それぞれのブロックがカラムに対応している。

>>> default_df._data
BlockManager
Items: Index(['    fruits', 'price'], dtype='object')
Axis 1: RangeIndex(start=0, stop=3, step=1)
ObjectBlock: slice(0, 1, 1), 1 x 3, dtype: object
NumericBlock: slice(1, 2, 1), 1 x 3, dtype: float64

上記から、デフォルトの NumPy バックエンドで作った DataFrame の BlockManager には object 型の ObjectBlock と float64 型の NumericBlock が登録されていることがわかる。

それぞれのブロックには DataFrame#_data#blocks という配列を経由してアクセスできる。 さらに、データの実装には values という変数でアクセスできる。

>>> default_df._data.blocks[0].values
array([['    apple', '    banana', '    cherry']], dtype=object)
>>> default_df._data.blocks[1].values
array([[198.,  nan, 398.]])

上記から、デフォルトの NumPy バックエンドで作った場合、ここに NumPy 配列が直接入ることが分かる。

NumPy Nullable バックエンド

続いては NumPy Nullable バックエンドを見ていこう。 先ほどと同じように DataFrame#_data で BlockManager にアクセスする。

>>> nullable_df._data
BlockManager
Items: Index(['    fruits', 'price'], dtype='object')
Axis 1: RangeIndex(start=0, stop=3, step=1)
ExtensionBlock: slice(0, 1, 1), 1 x 3, dtype: string
ExtensionBlock: slice(1, 2, 1), 1 x 3, dtype: Int64

すると、今度は string 型の ExtensionBlock と Int64 型の ExtensionBlock が登録されていることがわかる。

先ほどと同じように DataFrame#_data#blocks の配列から values 変数にアクセスしてみる。 先ほどは NumPy 配列が直接入っていたところだ。

>>> nullable_df._data.blocks[0].values
<StringArray>
['    apple', '    banana', '    cherry']
Length: 3, dtype: string
>>> type(nullable_df._data.blocks[0].values)
<class 'pandas.core.arrays.string_.StringArray'>
>>> nullable_df._data.blocks[1].values
<IntegerArray>
[198, <NA>, 398]
Length: 3, dtype: Int64
>>> type(nullable_df._data.blocks[1].values)
<class 'pandas.core.arrays.integer.IntegerArray'>

今度は NumPy 配列が入っているのではなく StringArray や IntegerArray という型のオブジェクトが入っていた。

この中で IntegerArray について、さらに _data_mask という変数にアクセスしてみよう。

>>> nullable_df._data.blocks[1].values._data
array([                 198, -9223372036854775808,                  398])
>>> nullable_df._data.blocks[1].values._mask
array([False,  True, False])

すると、今度は NumPy 配列が入っている。 これは _data の方が整数値を入れておく配列で _mask の方が欠損値かどうかを表した真偽値の配列になっている。 つまり「欠損値かどうかを表した配列」をデータとは別に用意することで、暗黙のキャストをしなくても欠損値を表現できるようにしている。

PyArrow バックエンド

PyArrow バックエンドについても同様に見ていこう。

>>> pyarrow_df._data
BlockManager
Items: Index(['    fruits', 'price'], dtype='object')
Axis 1: RangeIndex(start=0, stop=3, step=1)
ExtensionBlock: slice(0, 1, 1), 1 x 3, dtype: string[pyarrow]
ExtensionBlock: slice(1, 2, 1), 1 x 3, dtype: int64[pyarrow]

今度は string[pyarrow] 型の ExtensionBlock と int64[pyarrow] 型の ExtensionBlock が BlockManager に登録されている。

それぞれのブロックの values 変数にアクセスすると ArrowExtensionArray という型のオブジェクトになっている。

>>> pyarrow_df._data.blocks[0].values
<ArrowExtensionArray>
['    apple', '    banana', '    cherry']
Length: 3, dtype: string[pyarrow]
>>> type(pyarrow_df._data.blocks[0].values)
<class 'pandas.core.arrays.arrow.array.ArrowExtensionArray'>
>>> pyarrow_df._data.blocks[1].values
<ArrowExtensionArray>
[198, <NA>, 398]
Length: 3, dtype: int64[pyarrow]
>>> type(pyarrow_df._data.blocks[1].values)
<class 'pandas.core.arrays.arrow.array.ArrowExtensionArray'>

さらに _data 変数にアクセスすると、ここに pyarrow.lib.ChunkedArray 型のオブジェクトが入っている。

>>> pyarrow_df._data.blocks[1].values._data
<pyarrow.lib.ChunkedArray object at 0x13fcb6f70>
[
  [
    198,
    null,
    398
  ]
]

上記は PyArrow パッケージで提供されている Arrow フォーマットの配列になる。 このオブジェクトは見て分かるとおり整数型であっても欠損値を null で表現できる。

まとめ

今回は Pandas 2 系で導入されたデータ型のバックエンドという概念を紹介した。 それぞれのバックエンドはデータの持ち方が違うことから、内部的な処理のやり方についても異なっている。 もちろん、処理にかかる時間もそれぞれで異なる。

ちなみに Pandas 2.0.0 の時点で PyArrow バックエンドは最適化の余地が多く残されているようだ。 要するに、デフォルトの NumPy バックエンドの方が高速に動作するケースも多い。 これは Pandas の GitHub リポジトリに登録された Issue から確認できる。 もちろん、この点は今後バージョンが進むごとに改善されていくはずだ。

いじょう。

Python: Polars で文字列が省略される文字数を変更する

Polars は DataFrame や Series に含まれる文字列が、デフォルトでは 32 文字までしか表示されず、それを超えると省略されてしまう。 今回は、その挙動を変更する方法について書く。 結論から先に述べると pl.Config.set_fmt_str_lengths() を使うことで、省略されずに表示される最大の文字数を指定できる。

使った環境は次のとおり。

$ sw_vers    
ProductName:        macOS
ProductVersion:     13.2.1
BuildVersion:       22D68
$ python -V            
Python 3.9.16
$ pip list | grep polars
polars            0.16.14

もくじ

下準備

あらかじめ Polars をインストールしておく。

$ pip install polars

Python のインタプリタを起動する。

$ python

Polars をインポートする。

>>> import polars as pl

文字列が省略される挙動を確認する

まずは、文字列が省略される挙動を確認する。

次のように 32 文字までは省略されずに表示される。

>>> pl.DataFrame({"a": ["a"*32]})
shape: (1, 1)
┌──────────────────────────────────┐
│ a                                │
│ ---                              │
│ str                              │
╞══════════════════════════════════╡
│ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa │
└──────────────────────────────────┘

しかし 33 文字以上になると、次のように後続の文字列が「...」と省略されてしまう。

>>> pl.DataFrame({"a": ["a"*33]})
shape: (1, 1)
┌─────────────────────────────────────┐
│ a                                   │
│ ---                                 │
│ str                                 │
╞═════════════════════════════════════╡
│ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... │
└─────────────────────────────────────┘

省略されずに表示される文字数の最大値を変更する

続いて pl.Config.set_fmt_str_lengths() を使って文字列が省略されずに表示される文字数の最大値を変更してみよう。

以下では最大値を 40 に設定している。

>>> pl.Config.set_fmt_str_lengths(n=40)
<class 'polars.cfg.Config'>

設定した状態で 40 文字の文字列を作ってみよう。

>>> pl.DataFrame({"a": ["a"*40]})
shape: (1, 1)
┌──────────────────────────────────────────┐
│ a                                        │
│ ---                                      │
│ str                                      │
╞══════════════════════════════════════════╡
│ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa │
└──────────────────────────────────────────┘

ちゃんと省略されずに表示された。

では、設定した値を 1 文字超える文字列を作ってみる。

>>> pl.DataFrame({"a": ["a"*41]})
shape: (1, 1)
┌─────────────────────────────────────────────┐
│ a                                           │
│ ---                                         │
│ str                                         │
╞═════════════════════════════════════════════╡
│ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... │
└─────────────────────────────────────────────┘

今度は超えた部分は「...」と省略された。

いじょう。

参考

pola-rs.github.io