外部記憶装置

脳みそ小さすぎるからメモしとく

MPTCP を触ってみる(Ubuntu 22.04 LTS)

概要

Ubuntu 22.04 LTS (Linux Kernel 5.15) がリリースされ、通常用途のサーバーでも Linux Kernel 5.6 で実装された MPTCP の利用が容易になった。 せっかくなので、3つのシナリオで MPTCP を用いたアプリケーションを動かし、その動作の様子を観察した。 (※通学に利用している電車のWiFiが不安定で何とかしたいという理由もある)

MPTCP について

datatracker.ietf.org www.multipath-tcp.org Multipath TCPは複数パスを用いてコネクションを張ることで、TCPスループットや通信品質を改善することを目的としている。 利用用途としては、モバイル回線と固定回線の併用や複数のモバイル回線の併用といった、通信が不安定になりがちな回線におけるTCPの品質改善に利用されることを想定しているようだ。

MPTCP の使い方

MPTCP を利用するには、 socket(2)IPPROT_MPTCP フラグを指定してソケットを作成する必要がある。

int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_MPTCP);

そのため、既存のアプリケーションでは、socket(2) の引数を適切に書き換える必要がある。 RedHat では、既存のアプリケーションをMPTCPに対応させるために、SystemTapを用いた方法を説明している。

access.redhat.com

SystemTap を用いる方法は少し面倒なので、 今回は、 SystemTap を利用する代わりにTCPプロキシで通常のTCPの接続をMPTCPに中継する方法を取った。

MPTCP-Proxy

TCP の接続を終端し、MPTCPで接続を張り中継するだけなので、Go言語で簡易的に実装した。 (すでに既存実装があるような気がするが、探すのも面倒だったので)

github.com

動作としては、単純なTCPのプロキシである。 mode = "client" では、TCP -> MPTCP の中継を行い、mode = "server" では、 MPTCP -> TCP の中継を行う。

実験環境

Docker と docker-compose を用いて実験環境を用意した。 ホストは Ubuntu 22.04 LTS で、以下の設定となっている。

$ sudo sysctl -a | grep mptcp
net.ipv4.tcp_available_ulp = espintcp mptcp tls
net.mptcp.add_addr_timeout = 120
net.mptcp.allow_join_initial_addr_port = 1
net.mptcp.checksum_enabled = 0
net.mptcp.enabled = 1
net.mptcp.stale_loss_cnt = 4

$ sudo sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

実験にあたり、以下の資料を参考にした。

access.redhat.com

実験1(単純な2パス)

図のような環境で様子を観察した。本実験はMPTCP-Proxytest/simpleにあるスクリプト一式で行った。

$ cd test/simple
$ docker-compose up -d

で動かすことができる。

MPTCP 関連の設定

クライアント側

クライアント側では、MPTCP-Proxy5555/tcpTCP接続を待ち受け、10.123.200.2:4444 へMPTCPで接続する。

$ mptcp-proxy -m client -p 5555 -r 10.123.200.2:4444

MPTCP関連の設定として、サブフローを1つまで受け入れる設定をしている。

$ ip mptcp limits set subflow 1 add_addr_accepted 1

サーバー側

サーバー側では iperf3 が接続を待ち受け、 MPTCP-Proxy4444/tcp でMPTCP接続を待ち受け、localhost:5201TCPで接続する。

$ iperf3 -s &
$ mptcp-proxy -m server -p 4444 -r localhost:5201

MPTCP関連の設定として、サブフローを1つまで受け入れる設定をしている。 また、10.123.201.2(eth1) をMPTCPのエンドポイントとして利用する設定も行っている。

$ ip mptcp limits set subflow 1
$ ip mptcp endpoint add 10.123.201.2 dev eth1 signal

実験

iproute2 でMPTCPの接続状況を監視することができる。

$ docker-compose exec client ip mptcp monitor

別コンソールで iperf3 を動作させる。

$ docker-compose exec client iperf3 -c localhost -p 5555
Connecting to host localhost, port 5555
[  5] local 127.0.0.1 port 35948 connected to 127.0.0.1 port 5555
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-1.00   sec  1.10 GBytes  9.46 Gbits/sec    0   1.37 MBytes       
[  5]   1.00-2.00   sec  1.10 GBytes  9.43 Gbits/sec    0   1.37 MBytes       

mptcp monitor を用いた動作状況の確認

iproute2 mptcp monitor でも接続状況が反映される。

$ docker-compose exec client ip mptcp monitor
[       CREATED] token=c53b6fd4 remid=0 locid=0 saddr4=10.123.200.3 daddr4=10.123.200.2 sport=53360 dport=4444
[   ESTABLISHED] token=c53b6fd4 remid=0 locid=0 saddr4=10.123.200.3 daddr4=10.123.200.2 sport=53360 dport=4444
[     ANNOUNCED] token=c53b6fd4 remid=1 daddr4=10.123.201.2 dport=0
[SF_ESTABLISHED] token=c53b6fd4 remid=1 locid=0 saddr4=10.123.201.3 daddr4=10.123.201.2 sport=39957 dport=4444 backup=0
[       CREATED] token=c03b1893 remid=0 locid=0 saddr4=10.123.200.3 daddr4=10.123.200.2 sport=53362 dport=4444
[   ESTABLISHED] token=c03b1893 remid=0 locid=0 saddr4=10.123.200.3 daddr4=10.123.200.2 sport=53362 dport=4444
[     ANNOUNCED] token=c03b1893 remid=1 daddr4=10.123.201.2 dport=0
[SF_ESTABLISHED] token=c03b1893 remid=1 locid=0 saddr4=10.123.201.3 daddr4=10.123.201.2 sport=33487 dport=4444 backup=0

主フロー(Master flow) が 10.123.200.3 <-> 10.123.200.2 間で確立されている。 サーバー側から 10.123.201.2 へMPTCPによる接続が可能である通知が行われると、10.123.201.3 <-> 10.123.201.2 間でサブフロー(Sub flow) の接続が確立されていることが分かる。

クライアント(10.123.200.3/24, eth0) でのパケットキャプチャ

3-way handshake が MPTCP Multipath Capable フラグ付きで行われていることが分かる。

3-way handshake 直後に、サーバーから MPTCP Add Address オプションにより、 10.123.201.2 へのMPTCP接続をクライアントに対して通知していることが分かる。

クライアント(10.123.201.3/24, eth1) でのパケットキャプチャ

クライアント側のルーティングテーブルに従い、 10.123.201.2への接続は10.123.201.3/24のアドレスが割り当てられてインタフェース(ここではeth1)が利用される。 クライアント側から、10.123.201.2MPTCP Join Connection オプション付きで 3-way handshake が行われていることが分かる。

実験2(クライアントがシングルホーム)

図のような環境で様子を観察した。本実験はMPTCP-Proxytest/routingにあるスクリプト一式で行った。

$ cd test/routing
$ docker-compose up -d

で動かすことができる。

MPTCP 関連の設定

実験1 と同じである。

ルーティング

スタティックルーティングの設定を行っている。

クライアント側

ip route add 10.123.200.0/24 via 10.123.202.2
ip route add 10.123.201.0/24 via 10.123.202.2

サーバー側

ip route add 10.123.202.0/24 nexthop via 10.123.200.3 weight 1 nexthop via 10.123.201.3 weight 1

サーバー側については、ECMP(Equal Cost Multipath)で設定を行っている。 本来は、ip rule でエンドポイントのアドレスが割り当てられたインタフェースからパケットが送信されるように設定すべきな気がしている。

実験

実験1と同様の手順で実験を行った。

mptcp monitor を用いた動作状況の確認

iproute2 mptcp monitor でも接続状況が反映される。

$ docker-compose exec client ip mptcp monitor
[       CREATED] token=67e9a281 remid=0 locid=0 saddr4=10.123.202.3 daddr4=10.123.200.2 sport=46050 dport=4444
[   ESTABLISHED] token=67e9a281 remid=0 locid=0 saddr4=10.123.202.3 daddr4=10.123.200.2 sport=46050 dport=4444
[     ANNOUNCED] token=67e9a281 remid=1 daddr4=10.123.201.2 dport=0
[SF_ESTABLISHED] token=67e9a281 remid=1 locid=0 saddr4=10.123.202.3 daddr4=10.123.201.2 sport=47341 dport=4444 backup=0
[       CREATED] token=ab7a4b72 remid=0 locid=0 saddr4=10.123.202.3 daddr4=10.123.200.2 sport=46052 dport=4444
[   ESTABLISHED] token=ab7a4b72 remid=0 locid=0 saddr4=10.123.202.3 daddr4=10.123.200.2 sport=46052 dport=4444
[     ANNOUNCED] token=ab7a4b72 remid=1 daddr4=10.123.201.2 dport=0
[SF_ESTABLISHED] token=ab7a4b72 remid=1 locid=0 saddr4=10.123.202.3 daddr4=10.123.201.2 sport=59891 dport=4444 backup=0

主フロー(Master flow) が 10.123.202.3 <-> 10.123.200.2 間で確立されている。 サーバー側から 10.123.201.2 へMPTCPによる接続が可能である通知が行われると、10.123.202.3 <-> 10.123.201.2 間でサブフロー(Sub flow) の接続が確立されていることが分かる。

サーバー(10.123.200.2/24, eth0) でのパケットキャプチャ

3-way handshake 直後に、サーバーから MPTCP Add Address オプションにより、 10.123.201.2 へのMPTCP接続をクライアントに対して通知していることが分かる。 また、クライアント(10.123.202.3) <-> サーバー(10.123.200.2) の通信のみが行われていることが分かる。

サーバー(10.123.201.2/24, eth1) でのパケットキャプチャ

クライアント側から、10.123.201.2MPTCP Join Connection オプション付きで 3-way handshake が行われていることが分かる。 また、クライアント(10.123.202.3) <-> サーバー(10.123.201.2) の通信のみが行われていることが分かる。 LinuxのECMPにおいて送信元アドレスがどのような挙動(必ずアドレスが割り当てられたインタフェースから送信されるか)を示すかはさらなる調査が必要ではあるものの、2つのインタフェースに分配して通信が行われていることが分かる。

実験3(サーバーがシングルホーム)

図のような環境で様子を観察した。本実験はMPTCP-Proxytest/routing2にあるスクリプト一式で行った。

$ cd test/routing2
$ docker-compose up -d

で動かすことができる。

MPTCP 関連の設定

クライアント側

ip mptcp limits set subflow 1 add_addr_accepted 1
ip mptcp endpoint add 10.123.202.3 dev eth1 subflow

クライアント側からMPTCP subflow の接続を行う設定を行っている。

サーバー側

ip mptcp limits set subflow 1

ルーティング

スタティックルーティングの設定を行っている。

クライアント側

ip route add 10.123.200.0/24 via 10.123.201.2 src 10.123.201.3 metric 10
ip route add 10.123.200.0/24 via 10.123.202.2 src 10.123.202.3 metric 11

送信元アドレスが割り当てられたインタフェースから送信されるように経路を設定している。

ip rule で設定していたところ、クライアントのルーティングは利用できない様子?

サーバー側

ip route add 10.123.201.0/24 via 10.123.200.3
ip route add 10.123.202.0/24 via 10.123.200.3

実験

実験1と同様の手順で実験を行った。

mptcp monitor を用いた動作状況の確認

iproute2 mptcp monitor でも接続状況が反映される。

$ docker-compose exec client ip mptcp monitor
[       CREATED] token=e24736a9 remid=0 locid=0 saddr4=10.123.201.3 daddr4=10.123.200.2 sport=45618 dport=4444
[   ESTABLISHED] token=e24736a9 remid=0 locid=0 saddr4=10.123.201.3 daddr4=10.123.200.2 sport=45618 dport=4444
[SF_ESTABLISHED] token=e24736a9 remid=0 locid=1 saddr4=10.123.202.3 daddr4=10.123.200.2 sport=56857 dport=4444 backup=0 ifindex=533
[       CREATED] token=4d43f0e6 remid=0 locid=0 saddr4=10.123.201.3 daddr4=10.123.200.2 sport=45620 dport=4444
[   ESTABLISHED] token=4d43f0e6 remid=0 locid=0 saddr4=10.123.201.3 daddr4=10.123.200.2 sport=45620 dport=4444
[SF_ESTABLISHED] token=4d43f0e6 remid=0 locid=1 saddr4=10.123.202.3 daddr4=10.123.200.2 sport=45035 dport=4444 backup=0 ifindex=533

主フロー(Master flow) が 10.123.201.3 <-> 10.123.200.2 間で確立されている。 その後、10.123.202.3 <-> 10.123.200.2 間でサブフロー(Sub flow) の接続が確立されていることが分かる。

クライアント(10.123.201.3/24, eth0) でのパケットキャプチャ

3-way handshake が行われた後、サーバー、クライアント互いに新しいサブフローを張るために必要な情報を交換している様子は観察できなかった。 RFC8684 を読んだところ、2.2. Associating a New Subflow with an Existing MPTCP Connection に従っていそう。 通信はクライアント(10.123.201.3) <-> サーバー(10.123.200.2) の通信のみが行われていることが分かる。

クライアント(10.123.202.3/24, eth1) でのパケットキャプチャ

クライアントから MPTCP Join Connection オプション付きで 3-way handshake が行われていることが分かる。 また、クライアント(10.123.202.3) <-> サーバー(10.123.200.2) の通信のみが行われていることが分かる。

まとめ

3つのシナリオを取り上げて MPTCP の通信の様子を観察し、 通信がそれぞれのパスを利用して行われていることは確認できた。 実際に、4G(5G) と WiFi といった組み合わせの場合や、サブフローごとの重みの設定といった細かい制御が出来ると利用用途が広がりそうである。

P.S. Linux のルーティングよく分からん。

2021年を400文字で振り返る

2021年もあと数時間で終わるから今年やったことを400文字にまとめる。

卒論

1月に卒論を提出して無事卒業できた。 外部の研究会発表の締め切りと卒論の締め切りが1週間の中に同居してて大変だった。

学会発表

3月にIA研究会、6月にDICOMO 2021 シンポジウムで発表した。 DICOMOのナイトテクニカルセッションですべったのはいい思い出。 期末試験、レポートが重なる期間中に国際会議に出した論文が reject されてへこんだ。

セキュリティキャンプ

セキュリティキャンプ全国大会2021オンラインでL-III「準同型暗号アプリケーション開発ゼミ 」の講師をさせていただいた。 初めての講師で大変なことばかりだったが、とてもいい経験になった。

その他

1年を通して体重が増えた。来年は国際会議に論文通して海外に行きたい。

Android 12 で EAP-TLS の認証ができないときの対処方法

結論: 証明書の再インストール

大学に設置されている無線APではクライアント証明書による EAP-TLS 認証を利用しているが、 Android 12 にアップデートした Pixel 3a では認証できなくなったため問題を調査した。

状況

  • Android 12 にアップグレードした Pixel 3a で Wi-FiEAP-TLS 認証ができない
  • Android 11 の端末や新しく証明書をインストールした場合は認証が通る

Android のバージョン情報

f:id:PiBVT:20211112152418p:plain

問題調査

ADB shell 経由 logcat | grep wpa_supplicant で認証失敗時の wpa_supplicant のログを吸い上げた。

11-12 14:24:25.867   766   766 I wpa_supplicant: wlan0: Associated with (対象APのBSSID)
11-12 14:24:25.867   766   766 I wpa_supplicant: wlan0: CTRL-EVENT-EAP-STARTED EAP authentication started
11-12 14:24:25.868   766   766 I wpa_supplicant: wlan0: CTRL-EVENT-SUBNET-STATUS-UPDATE status=0
11-12 14:24:25.882   766   766 I wpa_supplicant: wlan0: CTRL-EVENT-EAP-PROPOSED-METHOD vendor=0 method=13
11-12 14:24:25.883   766   766 I wpa_supplicant: tls_connection_set_params: Clearing pending SSL error: error:0c00009e:ASN.1 encoding routines:OPENSSL_internal:NESTED_ASN1_ERROR
11-12 14:24:25.884   766   766 W wpa_supplicant: EVP_PKEY_from_keystore2:347 Keystore backend used with legacy alias prefix - ignoring.
11-12 14:24:25.886   766   766 E wpa_supplicant: EVP_PKEY_from_keystore2:389 Failed to parse x509 certificate.
11-12 14:24:25.886   766   766 E wpa_supplicant: ENGINE: cannot load private key with id 'USRPKEY_(証明書の名前)' [error:0c0000be:ASN.1 encoding routines:OPENSSL_internal:WRONG_TAG]
11-12 14:24:25.886   766   766 I wpa_supplicant: TLS: Failed to initialize engine
11-12 14:24:25.886   766   766 I wpa_supplicant: TLS: Failed to set TLS connection parameters, error code: -2
11-12 14:24:25.886   766   766 I wpa_supplicant: EAP-TLS: Failed to initialize SSL.
11-12 14:24:25.887   766   766 I wpa_supplicant: wlan0: CTRL-REQ-PIN-0:PIN needed for SSID (対象APのSSID)
11-12 14:24:25.887   766   766 I wpa_supplicant: wlan0: EAP: Failed to initialize EAP method: vendor 0 method 13 (TLS)

まず NESTED_ASN1_ERROR について調査したが情報が見つからなかったのでスルーした。

次に、EVP_PKEY_from_keystore2:389 Failed to parse x509 certificate. について調査したところ、Android 12 で導入された keystore2 のコミットログにそのような文字列が見つかった。

android.googlesource.com

f:id:PiBVT:20211112151802p:plain

コミットログを見たところ、

Keystore 2.0 engine: Handle legacy PEM certificates.

Keystore 2.0 in Android S requires all new certificates to be stored in
DER format, however, when upgrading from R or older, there may be
certificates stored in PEM format. This patch allows keystore2-engine to
extract the public keys from certificates in either format.

とあることから、Android 12 では証明書を管理するフォーマットが変わっており、このコミットで古いフォーマットについてもハンドルするようにしたことがわかる。 しかし、手元の端末では該当コミットで削除された Failed to parse x509 certificate. が出力されていることや、システムアップデートの日時以降のコミットであることから、 現在の Android 12 では この該当コミットが取り込まれていない可能性がある。

対応

古いフォーマットの証明書が読めていない可能性が高いため、再インストールすれば問題は解決するはずである。 結果、証明書を一度削除してから再インストールしたところ、無事接続に成功するようになった。

2020 年を雑に振り返る

みなさん、いかがお過ごしでしょうか? PiBVT です。

怒涛の 2020 年もあと数時間で終わるため、紅白を見ながら今年やったことについてまとめてみました。

今年あったこと

今年は公私ともに色々ありすぎて大変でした。 その中でも特に印象的だった以下の2つの事柄について取り上げます。

  • 未踏
  • 研究室・卒業研究

未踏

2019年6月から2020年2月までの期間中は未踏プロジェクトとして @nimdanaoto と @ushitora_anqou と一緒に取り組んでいました。 詳しいことは彼らの記録に任せますが、まぁ大変でした。

virtualsecureplatform.github.io

github.com

anqou.net

anqou.net

単純に成果を上げることだけでなく、成果をいかに伝えるかやアピールといった表現部分に大きな労力を費やしました。 また、全員がそのようなことは経験したことがないので、暗中模索するうちに意見が衝突することが多発し、精神的にも厳しいものがありました。

そんなチームが崩壊寸前だった中、僕らをつなぎとめてくれたのは「Factorio」というゲームの存在です。

とある惑星に墜落した宇宙船の船員が、原住民の土地を侵略し資源を略奪しながら脱出用のロケットや衛星を飛ばすというアメリカの開拓者を乗せたメイフラワー号顔負けの工場経営SLGです。

store.steampowered.com

未踏のミーティングが東京であった夜はホテルで反省会をした後に Factorioマルチプレイをすることで 互いの親睦を深め、「Factorio をするためにチームとして存在している」といっても過言ではない状態でした。

無事未踏のプロジェクト期間を終えた後は、成果を論文としてまとめる作業をしていました。 @nimdanaoto が筆頭で論文を書き、 @ushitora_anqou と僕が論文に必要な実装や計測を行っていました。 その中でも、文章表現をめぐり内紛が勃発することがありましたが、そんな日の夜はみんなで Factorio をして仲直り(?)をしていました。

その時書いていた論文は USENIX Security に採択され、来年はバンクーバーに行くのが楽しみです。(行けない可能性が高そうではあるけれど。)

www.usenix.org

来年以降も、このチームで少しずつ活動して SCO(Secure Computation Offloading) だけでなく他の分野にも応用できるように頑張っていきたいと思っています。

研究室・卒業研究

例年に1か月遅れて 4 月下旬に京都大学学術情報メディアセンター 岡部研究室に配属されました。

もともと自分自身の興味やアルバイトの担当分野がネットワーク(L2~L4) であったことからこの研究室を選びましたが、 結論としては自分にとって最高の研究室を選択したなぁと感じています。 研究室の先生だけでなく、修士の先輩と話していても面白い話や、かなり研究の専門的な部分に踏み込んだ相談であっても的確なアドバイスを頂けて とても楽しいです。ある程度の自由が効くので色々とやっていました。

pibvt.hateblo.jp

卒業研究に向けていろいろと研究しているのですが、先行研究を調べてまとめて内部のアーキテクチャを考えていると、 頭が爆発して一定期間何も手につかない状態になることがありました。

そんな時に僕の疲れた頭を癒してくれたのが「Factorio」というゲームです。 とある惑星に墜落した宇宙船の船員が、工場を建てまくって環境汚染をしたり、先住民の巣に核弾頭を叩き込んで先住民の反発を抑え込んだりする非常に友好的で健全な工場経営SLGです。

store.steampowered.com

日中は研究をして夜は2時間ほど未踏チームや他の友人と遊んでリフレッシュをしていました。 研究ばかりではなく、適度な息抜きが肝心であるということです。

卒業研究は卒論の執筆が若干遅れていますが、できる限り頑張って良い論文に仕上げて卒業したいです。

最後に

Factorio というゲームはあらゆる障壁を打ち破ることができるので、ぜひ遊んでください!

store.steampowered.com

2020年はお世話になりました。 それではみなさん、よいお年をお迎えください。

L3 技術で研究活動を快適にした話

これは CAMPHOR- Advent Calendar 2020 の12日目の記事です

advent.camph.net

はじめに

どうも、 PiBVT です。CAMPHOR- は11月ごろに初めて行ったばかりで、最近は研究が忙しくて行けてない状態です。

学生エンジニアの方が多く訪れる CAMPHOR- のアドベントカレンダーなので、技術的なことについて書いていきます。

免責事項

当記事の著者 PiBVT は当記事に基づいて行う一切の行為について、何らの責任を負うものではありません。

何をしたのか

研究室に配属された

今年からネットワーク関係の研究室に配属されたのですが、研究室に所属すると以下のメリットがあります

  • 大量の書籍
  • 自分専用のデスク
  • 自由に議論ができるスペース・ホワイトボード
  • やわらかいソファー

学部3回生までの生活とは一変し、研究室で生活できるのでは?と思うくらいに充実しています。

第1世代ネットワーク

院試に無事?合格したのち、研究室でお手伝いをするうちに研究室の VM サーバーを利用する権利を手に入れました。 研究室の VM サーバーでは結構強めで安定した VM を立てれるため、研究に関する開発の一切を研究室の VM 上で行うことにしました。

しかし、研究室の VM は学外からは専用の踏み台や VPN 経由で接続する必要があり、毎回接続するのは面倒です。 また、自宅サーバーに置いているデータを取り寄せるのも面倒です。

そこで、最初は VM と自宅のネットワーク間で VPN を張っていました。

f:id:PiBVT:20201210225228j:plain
第1世代ネットワーク

今回利用した VPN プロトコルが自宅ネットワークのコアルーターである RTX1200 に対応していなかったため、カプセリング用に別途 VyOS VM を用意しました。 自宅のルーターに VyOS を経由する研究室 VM への経路を静的経路として追加することで、自宅のネットワークと VM 間がシームレスに通信できるようになり、非常に快適になりました。

※図中の IP アドレスや AS 番号などはすべて仮のものです。図の通りに運用しているわけではありません。 「セキュリティがなってない!けしからん!」などの意見はご遠慮ください。書いてないだけなので。

※KUINS3 (京都大学の学内ネットワーク) では特定の VPN プロトコルで対外接続が可能です。

第2世代ネットワーク

せっかくネットワークの研究室に属しているのですから、けしからんことをしたくなるのが当然です。 先生や先輩と色々雑談しているうちに、以下の機材・権利を手に入れました。

  • Cisco の小型スイッチ・ルーター
  • 研究室内で割り当てられるプライベート v4 アドレスセグメント
  • (そこそこ自由に)研究室のネットワーク機器を操作する権利

弊研究室は、各個人にセパレーターで区切られた個人的なデスク・環境が割り当てられています。 機材を置くのに十分なスペースがあったため、デスクサイドに持ち込んだ Raspberry Pi などを利用して写真のような環境を構築しました。

f:id:PiBVT:20201210224957j:plain
デスクサイドの機材

機材を置けばそれらを家から操作したくなるのは当然のことなので、自分専用のオーバーレイネットワークは図のようになりました。

f:id:PiBVT:20201211021115j:plain
第2世代ネットワーク

デスクサイドと VM 間は L3 でトンネリングしていますが、VLAN 切り出せばよかったことに今気が付きました。 トンネリングをして、各ルーターに静的経路を設定すれば無事自宅からでもデスクサイドのネットワークに入れるようになりました。 研究室のデスクから自分のプライベートサブネットに接続すれば、自宅ネットワークにもそのままアクセス出来て快適です。

第3世代ネットワーク

便利なネットワークを構築できたことにより、研究も快適にできるようになりました(注:快適でも成果が出るわけではない)

しかし、ある日学内ネットワークの障害(一部対外接続の不通)により研究活動が停止してしまいました。 ネットワークの研究を行う学生として、ネットワークの障害で研究が止まるのはけしからん、 ということで学内のネットワークが死んだ場合でも大丈夫なネットワークへ改良することにしました。

研究室には学内ネットワークの KUINS 経由で外部に疎通する経路と、研究室独自の WIDE 経由で外部に疎通する経路(v6 のみ) があります。 そこで、 v6 をバックアップ経路として利用することにしました。

また、VPN で接続している自宅(S〇ftBank 回線)から大学の VM に到達する経路が東京を経由するというけしからんことが判明したため、 この問題にも対処することにしました。(SINET と直接接続している JPNAP と JPIX のトランジットポリシー?)

以下の図が最終的なネットワークの構成図となります。

f:id:PiBVT:20201210225541j:plain
第3世代ネットワーク

VyOS や FRRouting などのソフトウェアルーターを活用することで実費0円で構築することが出来ました。

対外接続時の経路切り替えは、各セグメントごとにプライベート AS 番号を割り当てて BGP を利用することで自動的に到達可能な経路になるようにしました。 デスクサイドと研究室VM の間で AS が異なるのはデスクサイドと VM 間が L3 でトンネルされていた影響です。 IGP は RIP を利用しています。簡単なネットワークなので。(フラグ)

自宅で契約している S〇ftBank 回線と SINET が東京でトランジットしている問題は、 さくらインターネットさんの大阪リージョンで VPS を借りてプライベートトランジットとして利用することで解決しています。

大阪にプライベートトランジットを置いた結果はグラフから確認できます。遅延がほぼ半分になり、大成功といえるでしょう

f:id:PiBVT:20201210225921p:plain
遅延の変化

しかし、さくらインターネットさんの VPS に大量のトラフィックを流すと迷惑なので、レイテンシが気になるもの(SSH など)や観測用の ICMP 以外は すべて大学・自宅間を直接結ぶ経路に流しています(Policy Based Routing)。大学・自宅間が不通になった場合はすべてのトラフィックVPS を流れますが。

f:id:PiBVT:20201210230006j:plain
PBR を適用

この構成にしてから何度か発生した学内ネットワークの障害も乗り越え、研究活動を継続できる超絶快適な環境が完成しました(注:環境が快適でも成果は(ry)

AS?BGP?

AS は Autonomous System の略で、管理のまとまりの単位となります。

www.nic.ad.jp

AS 間でお互いに BGP で経路情報を交換すること(ピアリング)により、互いのネットワークへの疎通が取れるようになります。 つまり、インターネットとは複数の AS 間のピアリングによって構成されているということです。

f:id:PiBVT:20201211134354j:plain
AS 間のピアリング

しかし、すべてのAS(一般に ISP が多い)がお互いにピアリングをするとなると、回線や管理費がかかりすぎて大変です。 Tire1 ISPと呼ばれる、全世界規模にネットワークを張り巡らした最上位のプロバイダが 国内の Tier2, Tier3 のISP や事業者をトランジット(ピアしている相手のトラフィックを交換する)ことで全世界のネットワークに到達することが出来るようになります。 国内では NTT Communications の GIN(Global IP Network) が Tier1 ISP として有名です。

また、トランジットサービスを様々な事業者に提供している組織を IX(Internet eXchange) といいますが、国内では JPIX, JPNAP, BBIX が有名です。

なぜ東京経由?

しかし、トランジット自体もお金がかかるので、事業者がかならずしもすべての IX と接続されているわけではありません。 そのため、 京都大学の上流ネットワークである SINET では、商用 IX としては JPIX と JPNAP としか接続されていません。

www.sinet.ad.jp

www.peeringdb.com

S〇ftbank のIP網からのトラフィックは JPIX か JPNAP を経由する必要がありますが、SINET という学術ネットワークに関するトラフィックを わざわざお金がかかる大阪(堂島)でトランジットするわけでもなく東京(大手町)でまとめてトランジットしていると考えるのが妥当でしょう。

f:id:PiBVT:20201211013013j:plain

一方で、SINET からさくらインターネット大阪リージョンの VPS に対するトラフィックは JPIX OSAKA 経由で、S〇ftbank からは直接到達できるようになっているため、 非常に低遅延で到達することができます。

プライベートトランジットサーバー

ここまでの話を整理すると

となります。

つまり、さくらインターネット大阪リージョンの VPS を経由すればトラフィックの経路が関西圏内に収まり、 遅延をさらに減らすことができると期待できます。

f:id:PiBVT:20201211133839j:plain
大阪でトランジット

そのためには、VPS 上でトラフィックを交換する必要がありますが、ごく簡単に言えば単なるルーティングでしかないので 実証自体は容易にできます。

しかし今回の目的は「研究環境を快適にする」ことなので、動的経路制御をするとなると プライベート AS 番号を割り当てて BGP でピアを張ること必要があるわけです。 そのため、前述のような研究室と自宅を結ぶには大掛かりなネットワークになりました。

Docker で実験

ここまでつらつらと僕自身の体験や経験を書いてきましたが、せっかくなので手軽に似たことを実験してみました。

有志の方によって VyOS の Docker コンテナが作られているため、このコンテナを利用して簡易的な実験ネットワークを組みました。

github.com

ネットワークはこの図のような構成になっています。

f:id:PiBVT:20201211153442j:plain
構築するネットワーク

インターネットを通じて両端の client-65001 と client-65002 が互いに疎通できるようになっています。 しかし、今回の経験を踏まえ、 vyos-65001, vyos-65002 間の経路には計 16ms の遅延が発生するようになっています。(値は大阪・東京間で計測した結果から雰囲気で決めました)

各 VyOS のコンフィグには以下の設定が入るようになっています。

# vyos-internet の設定ファイル
#!/bin/vbash
source /opt/vyatta/etc/functions/script-template
configure

# configure hostname
set system host-name vyos-internet

# configure VLAN interface
set interfaces ethernet eth0 vif 100 address 192.168.1.1/30
set interfaces ethernet eth0 vif 200 address 192.168.1.5/30

# configure latency emulation policy
set traffic-policy network-emulator DELAY-8 network-delay 8
set interfaces ethernet eth0 vif 100 traffic-policy out DELAY-8
set interfaces ethernet eth0 vif 200 traffic-policy out DELAY-8

# configure static route to each vyos
set protocols static route 192.168.100.0/24 next-hop 192.168.1.2
set protocols static route 192.168.200.0/24 next-hop 192.168.1.6

# 環境によって変えること
set protocols static route 0.0.0.0/0 next-hop 172.19.0.1

# configure nat
set nat source rule 1001 outbound-interface eth0
set nat source rule 1001 source address 192.168.100.0/24
set nat source rule 1001 translation address masquerade
set nat source rule 1002 outbound-interface eth0
set nat source rule 1002 source address 192.168.200.0/24
set nat source rule 1002 translation address masquerade

commit
exit
# vyos-65001 の設定ファイル
#!/bin/vbash
ip route del default

source /opt/vyatta/etc/functions/script-template
configure

set system host-name vyos-65001

# configure loopback
set interfaces loopback lo address 192.168.100.1/32

# configure VLAN interface
set interfaces ethernet eth0 vif 100 address 192.168.1.2/30
set interfaces ethernet eth0 vif 150 address 192.168.100.1/25

commit

# configure static route
set protocols static route 0.0.0.0/0 next-hop 192.168.1.1

commit
exit
# vyos-65002 の設定ファイル
#!/bin/vbash
ip route del default

source /opt/vyatta/etc/functions/script-template
configure

set system host-name vyos-65002

# configure loopback
set interfaces loopback lo address 192.168.200.1/32

# configure VLAN interface
set interfaces ethernet eth0 vif 200 address 192.168.1.6/30
set interfaces ethernet eth0 vif 250 address 192.168.200.1/25

# configure static route
set protocols static route 0.0.0.0/0 next-hop 192.168.1.5

commit
exit

各クライアントは起動時に VLAN を設定しています。

# client-65001 の設定
#!/bin/bash

# delete default route via docker network
ip route del default

# configure vlan
ip link add link eth0 name eth0.150 type vlan id 150
ip a add 192.168.100.2/25 dev eth0.150
ip link set dev eth0.150 up

# configure default route via vyos
ip route add default via 192.168.100.1

# wait for tty
while true
do
  sleep 1
done
# client-65002 の設定
#!/bin/bash

# delete default route via docker network
ip route del default

# configure vlan
ip link add link eth0 name eth0.250 type vlan id 250
ip a add 192.168.200.2/25 dev eth0.250
ip link set dev eth0.250 up

# configure default route via vyos
ip route add default via 192.168.200.1

# wait for tty
while true
do
  sleep 1
done

簡単に説明すると、各 VyOS は ISP (vyos-internet) の保有するセグメントの IP アドレスを割り振られているため、それを Interface に対して設定します。 vyos-6500x と vyos-internet 間のリンクは Docker ブリッジを毎回作るのが面倒なので VLAN を切っています。 そのため、まず指定された vlan id の vif (vlan interface) を作成し、それから vif に対してアドレスを割り当てます。

vyos-internet は 65001 と 65002 の経路情報が必要なため、static route でそれぞれのネットワークについて設定しています。 vyos-6500x では、デフォルトルートについて、 vyos-internet が外界に出れる GW となるので vyos-internet の IP アドレスを指定しています。

クライアントについても同様に、各ネットワークの vyos-6500x と VLAN で接続してデフォルトルートに向けるようにしています。

これで2拠点間を結ぶネットワークができました。疎通が取れていることも分かりますが、16ms の遅延があることが分かります。

f:id:PiBVT:20201211153547p:plain
ネットワーク間の疎通確認

では、プライベートトランジットとして vyos-65003 を追加してみましょう。

ネットワークの構成はこの図のようになっています

f:id:PiBVT:20201211171205j:plain
プライベートトランジットを利用するネットワーク

設定ファイルは以下の通りです。

# vyos-65001 の追加設定
set interfaces ethernet eth0 vif 103 address 192.168.250.2/30

# configure BGP
set protocols bgp 65001 address-family ipv4-unicast network 192.168.100.0/24

# configure BGP neighbor
set protocols bgp 65001 neighbor 192.168.250.1 remote-as 65003
# vyos-65002 の追加設定
set interfaces ethernet eth0 vif 203 address 192.168.250.6/30

# configure BGP
set protocols bgp 65002 address-family ipv4-unicast network 192.168.200.0/24

# configure BGP neighbor
set protocols bgp 65002 neighbor 192.168.250.5 remote-as 65003
# vyos-65003 の設定
#!/bin/vbash
ip route del default

source /opt/vyatta/etc/functions/script-template
configure

set system host-name vyos-65003

# configure loopback
set interfaces loopback lo address 192.168.250.1/32

# configure VLAN interface
set interfaces ethernet eth0 vif 103 address 192.168.250.1/30
set interfaces ethernet eth0 vif 203 address 192.168.250.5/30

# configure latency emulation policy
set traffic-policy network-emulator DELAY-3 network-delay 3
set interfaces ethernet eth0 vif 103 traffic-policy out DELAY-3
set interfaces ethernet eth0 vif 203 traffic-policy out DELAY-3

# configure BGP
set protocols bgp 65003 address-family ipv4-unicast network 192.168.250.0/24

# configure BGP neighbor
set protocols bgp 65003 neighbor 192.168.250.2 remote-as 65001
set protocols bgp 65003 neighbor 192.168.250.2 address-family ipv4-unicast nexthop-self

set protocols bgp 65003 neighbor 192.168.250.6 remote-as 65002
set protocols bgp 65003 neighbor 192.168.250.6 address-family ipv4-unicast nexthop-self

commit
exit

vyos-65001,vyos-65002 では vyos-65003 とピアリングするための BGP の設定を追加しています。 vyos-65003 はトランジットするために、各vyos-6500x から受け取った経路の nexthop を自分自身に設定した上で他のピアに対して再広報しています。 本来は route-map などで各ピアごとに広報する経路を制限するのが当然なのですが、今回は実験的なネットワークなので省略します。

設定ができたので実際に動かしてみます。 vyos-65001,vyos-65002 で vyos-65003 を介してそれぞれの経路が広報されていることが分かります。

f:id:PiBVT:20201211170734p:plain
vyos-65001 に BGP で広報された経路情報

traceroute などでも、広報された経路に基づいて経路選択が行われていることが分かります。 さらに、遅延は vyos-internet を介していた時よりも大きく減少していることも分かるかと思います。

f:id:PiBVT:20201211171411p:plain
プライベートトランジットを介する場合の経路と遅延

これで無事、プライベートトランジットを介した通信をすることが出来ました。

最後に

今回は L3 の技術を活用することで研究活動を快適にする環境を構築しました。 普通に開発をしているだけではこういった L2, L3 に触れる機会は少ないため、いい経験になりました。

最近は Docker などのコンテナで簡単に実験環境が構築出来て便利な一方、 現実の環境と異なり、理想的な環境しか提供してくれません。(カオスエンジニアリングのように障害等を注入することはもちろん可能ですが。)

今回は経路や障害といった現実の環境ならではの制約のもとでしたが、 一筋縄ではいかないために試行錯誤する過程は非常に楽しかったです。 少なからぬ研究の時間が吸い取られたことは事実ですが、 今後の研究に役立つかもしれない経験をできたこともまた事実です。 卒業研究が終わるまでは現環境で使い続けますが、春休みには研究室のインフラ刷新に合わせて改良するつもりです。

ここまで読んでくださった方の何かしらの役に立てば幸いです。それでは。

大学生活4年間を振り返るポエム

この記事は、Kyoto University Advent Calendar

adventar.org

の4日目の記事です。

はじめに

お久しぶりです。PiBVT です。 今年は未踏やら研究室配属やら濃厚な1年でした。

今年度で4回生となり、うまくことが進めば学部を卒業できるはずです。 せっかくなので、4年間の大学生活を振り返ってみたいと思います。

注意

圧倒的ポエムなので、こういった記事が嫌いな方は申し訳ありませんがブラウザバックの方をお願いします。

時系列順に振り返る大学生活

適当に1年ずつ振り返っていきます。

1回生の前期が怒涛すぎて2回生以降は手抜きです。

大学 0 回生

高校生の時は地方の県立高校で部活(柔道)をしつつ家では勉強、外に遊びに行くことはほとんどない(行く場所もない) ような生活でした。 高校生までは地元の古くからの友人も多く、それは楽しい毎日を送っていました。 自宅から高校まで往復で 25km ある道のりを毎日自転車で通学するのは大変でしたが...

無事前期試験で合格し、当時は何の具体的なプラン・ビジョンもなく、 「大学に行けば自分の好きなことを学べる!」という希望だけを胸に大学へと通い始めました。 そして、新学期早々、現実を突き付けられることになりました。

大学 1 回生

終わらぬ課題・加速する焦り

前期が始まり弊学科では、一つの講義が 90 分、多い日では1限(8:45~)から5限(~18:00) までありました。 当然各講義では課題が出されるわけですが、講義の内容すらよく理解できぬまま課題を解く羽目になり、朝までかかることもしばしばでした。 課題の量的に、京都大学工学部情報学科の学生にとって1回生の前期が最も辛いのではないかと思います。

さらに、物理学実験なる(実験自体は楽しいが)レポートの量がえげつない講義を受講していたため、 これも提出当日の朝までかかることが良くありました。

1回生前期は1週間のほとんどを課題に費やし好きなことを勉強できる時間はほとんどなく、 おまけに睡眠時間すらままならぬ状況でした。

また、家庭の事情で京都に下宿できず大阪から通学していたため、往復3時間かけて慣れない電車通学に体力を削られていきました。

孤独な闘い・削られる命

睡眠もままならぬ状態でそのような生活をしていて何も起こらないわけがありません。 半年で体重は 10kg 以上減り、精神的にも突然泣き始める、自殺願望などの異常が出てきました。

さらに、大阪からの通学という点で下宿をしている人に比べて気軽に相談できるような友人を持つことも出来ていない状態でした。

そんなこんなで、半年間は命を削りながら耐える綱渡り状態が続きました。

COVID-19 でリモート講義となっている1回生の方たちが辛い思いをしているのは痛いほど分かります。

終わる地獄

前期試験を終え、夏休みとなり地獄は終わりを迎えました。 夏休み中は反動で2か月間ずっとただダラダラ過ごす毎日が続きました。

夏休み期間中に自分の人生についていろいろと考え、 それまで学業を重視する考えを投げ捨て、自分のやりたいことをやることにしました。

もちろん、まったく勉強をしないわけではなく、無理をしない範囲で頑張りつつ自分のやりたいことを優先するという方針です。 高校生までは京都大学に行くためにやりたいことを押し殺して学業最優先で取り組んでいたわけであり、当時の自分としては大きな転換でした。

何事もなかった後期

夏休みで考え方を切り替え、できることをやりつつ自分をやりたいことをやっていました。

実のところ、いろいろと手を出していたので何をしていたのかはそれぞれ覚えていません。 唯一所属していたサークルの「機械研究会」にちょこちょこ顔を出すようになり、NF で作品を展示していたような気がします。

そんなこんなで1回生後期は追いつめられることもなく平和に過ごすことができました。

大学 2 回生

怒涛の1回生を終え、計算機コースに配属されました。 2回生では、それとなく講義を受けつつ色々とやりたいことをやっていました。 具体的には「アルバイト」と「ロボコン」です。

両親から食費として一定額を毎月もらっていたのですが、好きなことに使おうとすると自分で稼ぐ必要があります。 4月ごろに生協のアルバイト紹介で百万遍の近くにある会社がエンジニアのアルバイトを募集していたため、勉強になるかもと応募してみました。 面接をして無事採用されたのですが、初めての賃金労働で最初は分からないことばかりで大変でした。

サークルの方で「NHK学生ロボコン」という国内の学生ロボコンでは最大の大会に参加することになりました。 サークル内でメンバーの募集がかかった時、「なんとなく楽しそう」という楽観的な考えでメンバーとなりましたが、 実際にはかなり大変でした

学業は「上回履修」なる制度を利用して3回生配当科目でおもしろそうな講義を受講してたりしました。

大学 3 回生

やりたいことをやりつくした2回生は自分の人生においても大きな転換点でした。 3回生は講義自体が少なく、自由な時間が多いため何かしたいなぁと考えていたところ、 学生ロボコンのメンバーの一人から「未踏」なるプロジェクトに応募しようと声がかかったので グループで応募したところ、採択されたため、その開発に取り組んでいました。

専門性の高い講義や 計算機コース内では不評な計算機科学実験も楽しみながら受講していました。

大学 4 回生

無事そこそこの成績を収めていたため、第一志望の研究室に配属されました。 研究室ではIoT ネットワークのセキュリティについて研究しています。

研究自体も楽しいのですが、研究室に所属することで、 「研究室の計算資源を(ほぼ)自由に利用する」、 「研究室の機材を使って(ほぼ)自由に遊べる」 などの権利を獲得したため、存分に権利を活用して遊んでいます。

イベントごとに振り返る大学生活

アルバイト

2回生の4月から同じバイトを続けているのですが、アルバイト先の方の影響で 自分がやりたい研究分野が決まったり、 それまで知らなかった世界を知ることができたりなど、かなり良い影響を受けました。

仕事内容も研究っぽいことをして対外発表をする機会を頂くなど、大学に通っているだけでは出来ない経験もさせてもらいました。

色々と書きたいことはあるのですが、契約上書いてもいいか不明瞭なので今回はパスとします。

ロボコン

機械研究会というサークルで2回生の夏休みから3回生の夏休みの約1年間、NHK学生ロボコンに取り組んでいました。

機械研が学生ロボコンに出場するのは約15年ぶりで、技術的な資産がないだけでなく練習をする場所の確保すらままならない状態でした。 出場するために各メンバーが奔走して場所の確保やロボットの製作資金をかき集めるなど苦難の連続でした。

製作資金は OB の方からの寄付を頂き、感謝の限りです。 結局練習場所は、「ルネ」の横にあるサークル棟の屋上となり、1~4 月は寒空の下で夜通しロボットを動かしながらみんなでデバッグをしていました。

f:id:PiBVT:20201203224523j:plain

f:id:PiBVT:20201203224931j:plain

事前審査であるビデオ審査の締め切り直前では、 全員が限界まで知恵を絞って試行錯誤し、なんとか15年ぶりに本選出場が決まりました。

大会当日は「1回でも勝てれば万々歳」と言っていたのですが、実際の試合ではあれよあれよと勝ち進み、 訳が分からない状態で優勝しました。

NHK学生ロボコンでは優勝したチームが ABU ロボコンという世界大会(アジア・太平洋圏)に進出するのですが、 全く想定していなかった事態で翌日からドタバタと準備することになりました。

ロボットの改修だけでなく、メンバー全員分の旅費やその他手続きなどあれやこれやてんてこ舞いになりながらなんとか処理していました。

f:id:PiBVT:20201203225939j:plain

書き始めるときりがないので適当にはしょりますが、大会では 8 位となり、世界を相手にした貴重な経験が出来ました。 負けた試合の原因が僕の担当範囲の動作不良で自分の詰めの甘さを痛感しました。(試合前後の記憶があまりない)

海外のチーム(特に中国の代表チーム)の方たちとは自主的にホテルで意見交換会をしたりする中で 海外の学生のレベル・向上心の高さを目の当たりにし、身の引き締まる思いでした。

未踏

2019年度の未踏事業に応募し、採択されました。

www.ipa.go.jp

詳しい体験記はほかのメンバーの記事があります。

anqou.net

anqou.net

きっかけは、2019年の2月末に学生ロボコンで同じメンバーだった @nimdanaoto と機械研の工作室で 雑談をしていたところ「おもろいアイデアあるで。」となり、ちょうど未踏事業の応募期間だったため応募することにしました。

イデアを出すうちに「独自 ISA のコンパイラとかツールチェインを書ける人が欲しい」と なったので、偶然Cコンパイラを書ける深海魚 @ushitora_anqou がいたため チームに引き込み、3人で応募することになりました。

f:id:PiBVT:20201203232827p:plain

その後、採択の通知が届き、未踏事業として約9か月間取り組んでいました。 取り組んでいた内容は各種発表などで詳細にあるので割愛します。

github.com

相応の成果を求められながら独自のアイデアで開発を行う経験は初めてで、 期間中はメンバーとアイデアや実装方針を巡って口論になることが多々あり、 メンバーのうち1人は「もうあんな経験は二度としたくない」というほどにはお互いにギスギスすることもありました。

最後はうまくまとめ上げることが出来てよかったものの、一つ間違えば大変なことになっていたと思います。 その後、ありがたいことにスーパークリエーターに認定していただいたり、VSP に関する論文が USENIX Security '21 に採択されたりしました。 成果がいろいろな形で評価され、開発者としてはうれしい限りです。

www.usenix.org

最後に

1回生のころは一人で呆然としていたのですが、2回生以降は友人達のおかげで非常に濃厚な日々を過ごすことができとても感謝しています。 気軽に話せる友人がいると、いろいろと意見やアイデアの交換ができ一人では無理なことも出来るということを感じることが多々ありました。 大学生活4年間でそういったことを実際に体験できたのは貴重な経験だと思っています。 学部卒業後は現在所属している研究室で引き続き修士学生として研究に取り組んでいきます。

長々と駄文を垂れ流しましたが、読者の方に楽しんでいただけたのなら幸いです。

5日目は高林さんです。どのような記事を書かれるのか楽しみです。 それでは。

MBR を雑にダンプしてみた

動機

MBRを触ったことがあまりなかったので、勉強がてらに雑に読み出してダンプしてみました。

MBRの構造

MBRはディスクの先頭セクタ(512byte)に存在します。

Wikipediaによると、 構造は以下のようになっています。

ja.wikipedia.org

オフセット サイズ(byte) 内容
0x000 446 ブートストラップローダ
0x1BE 16 第1パーティションテーブル
0x1CE 16 第2パーティションテーブル
0x1DE 16 第3パーティションテーブル
0x1FE 16 第4パーティションテーブル
0x1FE 2 ブートシグネチャ(0xAA55)

今回は、ブートシグネチャパーティションテーブルを読み取ることにしました。 ブートシグネチャはリトルエンディアンで 0xAA55 で、パーティションテーブルは以下のようになっています。

オフセット サイズ(byte) 内容
0x0 1 ブートグラグ(0x80:ブート可 0x00:ブート不可)
0x1 3 パーティション
最初のセクタ
(CHS方式)
ヘッド
シリンダの上位2ビットと
セクタ
シリンダの下位8ビット
0x4 1 パーティション識別子
0x5 3 パーティション
最後のセクタ
(CHS方式)
ヘッド
シリンダの上位2ビットと
セクタ
シリンダの下位8ビット
0x8 4 パーティションの最初のセクタ(LBA方式)
0xC 4 パーティションの全セクタ数

CHS方式は、シリンダ・ヘッド・セクタを利用した表現で、LBA方式はセクタ(512byte)を単位とした表現です。 CHS方式は最大24bit, LBA方式は最大32bit のフィールドしか割り当てられていないため、 扱えるディスクサイズには限界があります。 それが 528MB, 8.4GB, 137GB の壁として問題になりました。 詳細はこの方のページが非常に参考になりました。 caspar.hazymoon.jp

MBRをダンプする

初めて触るGo言語でダンプツールを作成しました。 まだ触り始めなので、もっといい書き方があるに違いないのですが、この程度でも十分に機能してくれます。

処理の流れとしては、

だけです。それぞれのデータのオフセットは固定なので簡単にダンプすることが出来ます。 さらに、Linuxではブロックデバイスファイルを open して read することでディスクの中身を簡単に読み出すことが出来るので楽です。

実際のコードがこんな感じです。

package main

import (
    "fmt"
    "os"
    "encoding/binary"
)

func printMBRPartitionInfo(partitionName string, buf []byte) {
    fmt.Println(partitionName)
    fmt.Printf("- Type: %02X\n", buf[0x04])
    fmt.Printf("- BootFlag: %t\n", buf[0x00] == 0x80)

    fmt.Printf("- Sector Num: %d\n", binary.LittleEndian.Uint32(buf[0x0C:]))
    fmt.Printf("- First Sector(CHS): %02X %02X %02X\n", buf[0x01], buf[0x02], buf[0x03])
    fmt.Printf("- Last  Sector(CHS): %02X %02X %02X\n", buf[0x05], buf[0x06], buf[0x07])
    fmt.Printf("- First Sector(LBA): 0x%08X\n", binary.LittleEndian.Uint32(buf[0x08:]))
}

func main(){
    // ブロックデバイスを開く
    file, err := os.Open("/dev/sdb")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // LBA 0 を読み込む
    buf := make([]byte, 512)
    file.ReadAt(buf, 0)

    // ブートシグネチャの読み取り
    if (buf[0x1FE] != 0x55 || buf[0x1FF] != 0xAA){
        return
    }
    printMBRPartitionInfo("1st Partition", buf[0x1BE:])
    printMBRPartitionInfo("2nd Partition", buf[0x1CE:])
    printMBRPartitionInfo("3rd Partition", buf[0x1DE:])
    printMBRPartitionInfo("4th Partition", buf[0x1EE:])
}

Lubuntu のインストラーが入ったUSBメモリに対して実行したときの結果は以下のようになりました。

1st Partition
- Type: 00
- BootFlag: true
- Sector Num: 3475360
- First Sector(CHS): 00 01 00
- Last  Sector(CHS): 6A E0 F6
- First Sector(LBA): 0x00000000
2nd Partition
- Type: EF
- BootFlag: false
- Sector Num: 8000
- First Sector(CHS): FE FF FF
- Last  Sector(CHS): FE FF FF
- First Sector(LBA): 0x00349820
3rd Partition
- Type: 83
- BootFlag: false
- Sector Num: 12140544
- First Sector(CHS): 76 1F D8
- Last  Sector(CHS): 2D E1 CC
- First Sector(LBA): 0x00351000
4th Partition
- Type: 00
- BootFlag: false
- Sector Num: 0
- First Sector(CHS): 00 00 00
- Last  Sector(CHS): 00 00 00
- First Sector(LBA): 0x00000000

第1パーティションは Type 00 の空のパーティションですが、パーティションが確保されていることから何らかの専用パーティションと考えられます。 ブートフラグが立っているので、ここからブートします。

第2パーティションは Type EF の EFIパーティションです。UEFI用の bootx64.efi などが配置されています。

第3パーティションは Type 83 の Linux ファイルシステムです。今回はwritableな領域として、ログの保存等に利用されています。

第4パーティションは存在しません。

第1パーティションについて

さすがに気になるので詳細を調べてみたところ、 Lubuntuのディスクイメージは Mac を含むあらゆるシステムで動かすために、CDのファイルシステムである ISO 9660 と Apple Partition Map を含むようです。

askubuntu.com

実際に parted で調べたところ、 Apple フォーマットのパーティションとして認識されました。

$ sudo parted /dev/sdb p
警告: ドライバは物理ブロックサイズが 2048 バイトであると言っていますが、Linux は 512 バイトだと言っています。
無視(I)/Ignore/取消(C)/Cancel? I                                          
モデル: BUFFALO USB Flash Disk (scsi)
ディスク /dev/sdb: 32.0GB
セクタサイズ (論理/物理): 2048B/512B
パーティションテーブル: mac
ディスクフラグ: 

番号  開始    終了    サイズ  ファイルシステム  名前   フラグ
 1    2048B   6143B   4096B                     Apple
 2    1765MB  1769MB  4096kB                    EFI

GPT なディスクを読んでみた

GPT 形式のディスクも読み込んでみました。

1st Partition
- Type: EE
- BootFlag: false
- Sector Num: 1000215215
- First Sector(CHS): 00 01 00
- Last  Sector(CHS): FE FF FF
- First Sector(LBA): 0x00000001
2nd Partition
- Type: 00
- BootFlag: false
- Sector Num: 0
- First Sector(CHS): 00 00 00
- Last  Sector(CHS): 00 00 00
- First Sector(LBA): 0x00000000
3rd Partition
- Type: 00
- BootFlag: false
- Sector Num: 0
- First Sector(CHS): 00 00 00
- Last  Sector(CHS): 00 00 00
- First Sector(LBA): 0x00000000
4th Partition
- Type: 00
- BootFlag: false
- Sector Num: 0
- First Sector(CHS): 00 00 00
- Last  Sector(CHS): 00 00 00
- First Sector(LBA): 0x00000000

第1パーティションは Type EE となり、GPT形式のディスクであることを示しています。

このように、GPT形式のディスクでも LBA 0 にMBRが存在し、GPT形式であることを示しています。

まとめ

MBR のダンプ自体は非常に簡単で、ディスクに対する知見を得ることが出来ました。 せっかくなので、GPT形式のディスクについてもダンプを取りたいと思っています。

RustでMBRダンプをしてみた(2020/03/28 追記)

Rustにも興味があったのでRustを使ってダンプしてみました。

use std::io::prelude::*;
use std::fs::File;

// ここらへんもう少しなんとかならんかな
fn convert_bytes_to_u32_le(bytes:&[u8]) -> Result<u32, ()>{
    // サイズが違うときはエラーとする
    if bytes.len() != 4 {
        return Err(());
    }

    let mut u32_raw:[u8; 4] = [0; 4];
    u32_raw.clone_from_slice(&bytes[0..4]);
    let _u32 = unsafe { std::mem::transmute::<[u8; 4], u32>(u32_raw) }.to_le();
    return Ok(_u32);
}

fn print_mbr_partition_info(part_name:&str, buffer:&[u8]){
    // サイズが違うときは何も表示しない
    if buffer.len() != 0x10 {
        return;
    }

    println!("{}", part_name);
    println!("- Type: 0x{:X}", buffer[0x04]);
    println!("- BootFlag: {}", buffer[0x00] == 0x80);
    println!("- Sector Num: {}", convert_bytes_to_u32_le(&buffer[0x0C..0x10]).unwrap());
    println!("- First Sector(CHS): {:02X} {:02X} {:02X}", buffer[0x1], buffer[0x02], buffer[0x03]);
    println!("- Last  Sector(CHS): {:02X} {:02X} {:02X}", buffer[0x5], buffer[0x06], buffer[0x07]);
    println!("- First Sector(LBA): 0x{:08X}", convert_bytes_to_u32_le(&buffer[0x08..0x0C]).unwrap());
}

fn main() {
    let mut f = File::open("/dev/sdb").unwrap();

    let mut buffer:[u8; 512] = [0; 512];
    f.read(&mut buffer).unwrap();

    // シグネチャがマッチしなければ終了する
    if (buffer[0x1FE] != 0x55) || (buffer[0x1FF] != 0xAA){
        return;
    }

    print_mbr_partition_info("1st Partition", &buffer[0x1BE..0x1CE]);
    print_mbr_partition_info("2nd Partition", &buffer[0x1CE..0x1DE]);
    print_mbr_partition_info("3rd Partition", &buffer[0x1DE..0x1EE]);
    print_mbr_partition_info("4th Partition", &buffer[0x1EE..0x1FE]);
}