/home/tnishinaga/TechMEMO

日々行ったこと、面白かったことを書き留めます。

Raspberry Pi 5のRP1に搭載されているPIOは今のところ簡単には使えないという話

私が海外でPi5発売されてから楽しみにしていたことの一つは「RP1に搭載されているPIOが使えるか」です。

画像は https://datasheets.raspberrypi.com/rp1/rp1-peripherals.pdf のFigure2,pp.6 より引用(一部加筆)

海外でのPi5発売とほぼ同時期に公開されたRP1のマニュアルをみると、Cortex-M3コアにPIOが接続されている様子が示されていました。 また、同マニュアルのpp.12よりRP1のコアクロックは200MHzらしいです。 (PicoのRP2040と同じなら)PIOはコアクロックと同じ速度で動くはずなので、理論値で最大100MHzくらいの信号を出力できるかもしれず、ワクワクしていました。

時が経ち2024年2月13日、ついにPi5が日本でも発売され、嬉しいことに1台購入することができました。 そこで早速RP1のPIOを使う方法を調べてみたのですが、どうやら今のところ使えなさそうとわかりました。

以下、調べたことをつらつらと書いていきます。

(PIOについては手前味噌ですが こちらの資料 をご参照ください)

なぜPIOは使えないのか

一言で言えば「PIO制御用ペリフェラルがPCIeから見えるメモリにmapされていないから」らしいです。

Pi5でPIOが使えるかはMichaelBellさんがすでに調査して共有してくれています。

rp1-hacking/PIO.md at main · MichaelBell/rp1-hacking · GitHub

こちらには以下のように書かれています。

The PIO registers are accessible at address 0xf000_0000 from the RP1. Additionally the FIFOs (only) are accessible at 0x40178000 and hence from Linux (at 0x1f_00178000). rp1-hacking/PIO.md at main · MichaelBell/rp1-hacking · GitHub より引用

つまり、LinuxからはPIOのFIFOしかアクセスできず、PIOの制御レジスタはRP1のプロセッサからしかアクセスできないメモリにいるようです。 本当にそうなっているのでしょうか?

メモリマップの調査

PCIeデバイスの制御は、デバイスの持つメモリをホストのメモリにマップし、そこにアクセスして行います(参考: https://osdev.jp/wiki/PCI-Memo)。

RP1はPCIe経由で接続されているので、RP1のメモリもホストのメモリの何処かにマップされています。

どこにマップされているかは、以下の起動時のログやドライバを眺めたり、mmapして実際にメモリを読んでみると、 RP1の 0x4000_0000 がホストの 0x1F_0000_0000 にマウントされているとわかります。

[    1.767860] pci 0000:01:00.0: BAR 1: assigned [mem 0x1f00000000-0x1f003fffff]
[    1.775026] pci 0000:01:00.0: BAR 2: assigned [mem 0x1f00400000-0x1f0040ffff]
[    1.782192] pci 0000:01:00.0: BAR 0: assigned [mem 0x1f00410000-0x1f00413fff]
...
[    1.836165] rp1 0000:01:00.0: bar0 len 0x4000, start 0x1f00410000, end 0x1f00413fff, flags, 0x40200
[    1.845252] rp1 0000:01:00.0: bar1 len 0x400000, start 0x1f00000000, end 0x1f003fffff, flags, 0x40200
[    1.854518] rp1 0000:01:00.0: enabling device (0000 -> 0002)
[    1.861119] rp1 0000:01:00.0: chip_id 0x20001927

具体的には以下のように調べていきました。

Linuxから見えるPIOレジスタ?のチェック

RP1のマニュアルによるとPIOはRP1のメモリの0x40178000にマップされているらしいです。 そこでLinuxから 0x1f_0017_8000 にアクセスしたところ、こんな値が読めました。

(余談: このメモリは32bitずつしか読み込めないです。8bitずつ読み込むと上位24bitが0xffになります。なぜ...?)

# 左が 0x40178000 からのoffset、右が読み込んだ32bit値

map ok
read pio registers
0x000000 : 00000000
0x000004 : 00000000
0x000008 : 00000000
0x00000c : 00000000
0x000010 : 418c2041
0x000014 : 2d8ef0c6
0x000018 : 42c753b3
0x00001c : d61f584b
0x000020 : 70696f33
0x000024 : 00000000
0x000028 : 00000001
0x00002c : 00000000
0x000030 : 00000000
0x000034 : 00000000
0x000038 : 00000001

MichaelBellさんの調査結果より、RP1のPIOのFSTATレジスタはオフセット0x04にいるはずです。 このレジスタには何らかの値が入っているはずなので、0が読み出されるのはなにか変です。 よって、少なくともこのアドレスにPIOの制御レジスタがないことは確かそうです。

0xF000_0000 にあると言われているPIOレジスタLinuxから読めないのか

RP1のマニュアルの「Table 1. Address Map summary」と「Table 2. Peripheral Address Map」を見る限り 0xF000_0000 は外部に見えるアドレスが割り当てられてなさそうなので、厳しそうです。

画像は https://datasheets.raspberrypi.com/rp1/rp1-peripherals.pdf のTable 1. Address Map summary,pp.7 より引用

以上より、今のところPIOはLinuxから簡単に扱えなさそうという結論になりました。

MichaelBellさんはどうやってPIOを使おうとしているか

フォーラムでの投稿やgithubのコードを見る限りでは、G33katWorkさんのRP1リバースエンジニアリングの成果を元にRP1でPIOを制御するコードを動かそうとされているようです。

GitHub - G33KatWork/RP1-Reverse-Engineering: Experiments on the RP1

rp1-hacking/launch_core1 at main · MichaelBell/rp1-hacking · GitHub

まだコードをきちんと理解できていないのですが、どうもShared SRAMに自作のコードを置いたあと、WatchDogを制御してリセットをかけてRP1のCore1で自作コードを動かしているみたいです。

このやり方についてRaspberry Pi エンジニアのjdbさんは以下のように言われています。

You are free to hack around with the hardware, but for anyone else wanting to experiment with this, heed these warnings: The RP1 firmware expects unfettered access to the entirety of shared SRAM, starting at 0x2000_0000. Arbitrary modification of any part of the memory region may cause side-effects up to and including loss of data and hardware damage. Using RP1 and PIO on Raspberry Pi 5 - Raspberry Pi Forums より引用

意訳すると「やるのは自由だけど、壊れるかもしれないから気をつけてね」という感じでしょうか。

その後の投稿で「core1もそのうち使えるようにする」と言われているので、それを気長に待つと良いのかなと思います。 (こういうHackやるようにメモリ1Gだけどお手頃価格なPi5出たら嬉しいなー)

おしまい

以上です。

参考文献

baremetal Rustでcritical-sectionを使うメモ

趣味でRustを使ってarm64向けのBaremetal開発を行っています。 今日はその環境でdefmtを使おうとして躓いたのでメモを残しておきます。

critical-section

defmt_rttはcritical-sectionというcrateに依存しています。

critical-sectionはRustでatomicな処理をエミュレーションするためなどに使うcrateです。 defmtではログの出力部にでも使われているのでしょう。

https://crates.io/crates/critical-section

critical-sectionをビルドするには一部メソッドの実装が必要です。 ただし、これらはcortex-mなどのbaremetal開発で使う主要なcrateには実装済みなので、自分で実装が必要になることはほぼないと思います。

cortex-m/cortex-m/src/critical_section.rs at master · rust-embedded/cortex-m · GitHub

今回Baremetal Arm64用の実装は見つけられなかったので、自分で実装することにしました。

ちなみに、critical-sectionの一部メソッドを実装せずにビルドすると、リンカが以下のようなエラーを出してビルドに失敗します。

 function `critical_section::release':
          /home/tnishinaga/.cargo/registry/src/index.crates.io-6f17d22bba15001f/critical-section-1.1.2/src/lib.rs:200: undefined reference to `_critical_section_1_0_release'

SingleCoreCriticalSection

自分でcritical-section用の処理を実装する方法は、critical-sectionのREADMEにかかれています。

# critical-section v1.1.2, READMEより引用
# https://github.com/rust-embedded/critical-section/tree/7ac14a65144bea29eed06947f47b9aef51512cf4

# #[cfg(not(feature = "std"))] // needed for `cargo test --features std`
# mod no_std {
// This is a type alias for the enabled `restore-state-*` feature.
// For example, it is `bool` if you enable `restore-state-bool`.
use critical_section::RawRestoreState;

struct MyCriticalSection;
critical_section::set_impl!(MyCriticalSection);

unsafe impl critical_section::Impl for MyCriticalSection {
    unsafe fn acquire() -> RawRestoreState {
        // TODO
    }

    unsafe fn release(token: RawRestoreState) {
        // TODO
    }
}
# }

single coreでの実装例は以下です。 acquireで割り込みを禁止し、releaseで割り込みを再度有効化しています。 シングルコアで割り込み禁止して、atomicなメモリアクセスをエミュレーションしているようです。

// https://github.com/rust-embedded/cortex-m/blob/master/cortex-m/src/critical_section.rs より引用


use critical_section::{set_impl, Impl, RawRestoreState};

use crate::interrupt;
use crate::register::primask;

struct SingleCoreCriticalSection;
set_impl!(SingleCoreCriticalSection);

unsafe impl Impl for SingleCoreCriticalSection {
    unsafe fn acquire() -> RawRestoreState {
        let was_active = primask::read().is_active();
        interrupt::disable();
        was_active
    }

    unsafe fn release(was_active: RawRestoreState) {
        // Only re-enable interrupts if they were enabled before the critical section.
        if was_active {
            interrupt::enable()
        }
    }
}

ところで、私の作っているコードはまだ割り込み周りの実装を行っていないので割り込みは常に無効状態で、シングルコアでしか動かしていません。 ということは、acquireとreleaseの両方で何もしなくても大丈夫なはずです。 よって、ほぼサンプルそのままのコードを実装してcritical-sectionの実装は完了ということにしました。

以上です。

iperf3の実装を読んでRustで簡易版を実装してみた

iperfを使うことはあっても中の通信がどうなっているのか知らなかったので、調べてみました。

自作のコードで簡単なスループット計測が行えることをゴールとします。

環境

今回の調査はm1 mac book上でiperfを実行し、その通信内容をwiresharkで見ながら行いました。

iperfのバージョンは以下になります。

~ ❯❯❯ iperf3-darwin -v
iperf 3.8.1 -  -- Apple version iperf3-107 (cJSON 1.7.13)
Darwin hostname.local 23.1.0 Darwin Kernel Version 23.1.0: Mon Oct  9 21:28:12 PDT 2023; root:xnu-10002.41.9~6/RELEASE_ARM64_T8103 arm64
Optional features available: sendfile / zerocopy

コードは2023/12/22時点のものを参照しました。

github.com

通信の概要

通信手順の概要は以下のようになっています。(図がなくてすみません)。

  1. クライアントからサーバーにテストの設定を送る
  2. サーバーからクライアントにデータ送信開始の指示を出す
  3. クライアントからサーバーに一定量データを送る
  4. 結果をお互いに交換する

もう少し細かい手順を以下に示します。 cはclient、sはserver、矢印はどちら向きの通信かを表しています。 iperf3のテストではコマンド送受信用(cmd)とデータ通信用(data)の2つのコネクションを使用します。 そのため、どちらのコネクションを利用するかを通信方向の後に記します。

  1. c->s, cmd: Cookie(セッション識別用の文字列)を送る
  2. s->c, cmd: Param exchange(9)を送る
  3. c->s, cmd: 設定の書かれたJSONファイルサイズ(32bit, big endian)を送る
  4. c->s, cmd: JSONファイルの中身を送る
  5. s->c, cmd: CREATE_STREAMS(10)を送る
  6. c->s, data: データ通信用のコネクションを作り、そこから手順1と同一のCookieを送る
  7. s->c, cmd: TEST_START(1)を送る
  8. s->c, cmd: TEST_RUNNING(2)を送る
  9. c->s, data: データを送る
  10. c->s, cmd: TEST_END(4)を送る
  11. c->s, cmd: EXCHANGE_RESULTS(13)を送る
  12. c->s, cmd: JSONのファイルサイズ(32bit, big endian)をおくる
  13. c->s, cmd: 計測結果のJSONファイルを送る
  14. s->c, cmd: JSONのファイルサイズ(32bit, big endian)をおくる
  15. s->c, cmd: 計測結果のJSONファイルを送る
  16. c->s, cmd: IPERF_DONE(16)をおくる

以降、更に詳細について見ていきます。

コマンド

iperf3は1byteのコマンド(正確にはiperfのstate)を送って制御を行っています。

ここで使うコマンドは iperf_api.h の107行目付近に定義されています。

github.com

iperf3ではテストの識別のため、cookieと呼ばれるランダムな文字列を使います。 このCookieabcdefghijklmnopqrstuvwxyz234567 の中からランダムに選ばれた36文字と空文字の合計37byteで構成されています(参考元: iperf/src/iperf_util.c at ec06f7b43854153044c0a5e9ea2845e07262dcf8 · esnet/iperf · GitHub)。

文字テーブルから 01 が外されている理由は不明です。 理由をご存じの方がいたら教えてください。

設定のJSON

テストの序盤ではクライアントからサーバーに設定をJSONで送ります。 実際にiperf3-darwin -c localhost コマンドを実行すると以下のようなJSONが送られます。

{
    "tcp": true,
    "omit": 0,
    "time": 10,
    "parallel": 1,
    "len": 131072,
    "pacing_timer": 1000,
    "client_version": "3.8.1"
}

各設定の種類や詳細については send_parameters関数 から読んでいくと良さそうです。

最低限動けば良いレベルであれば、テスト時間を示すtimeとTCPのブロックサイズを決めるlenだけ注意していれば良かったです。 それ以外のomitやpacing_timerについては、どの部分に影響のある設定か把握しきれていません。

結果のJSON

計測が終わった後、サーバーとクライアントの両方で計測した結果を交換します。

実際にやり取りされているJSONはこんな感じになっています。

{
    "cpu_util_total": 85.569230769230771,
    "cpu_util_user": 10.461538461538462,
    "cpu_util_system": 75.1076923076923,
    "sender_has_retransmits": 1,
    "streams": [
        {
            "id": 1,
            "bytes": 506824,
            "retransmits": 0,
            "jitter": 0,
            "errors": 0,
            "packets": 0,
            "start_time": 0,
            "end_time": 0.000238
        }
    ]
}

streamsの中身は配列になっており、idは1番開始になっています。 オリジナルの実装では1秒毎の計測結果をstreamsに入れて送っているようですが、n秒分まとめて送っても受け付けてもらえました。

iperf/src/iperf_api.c at ec06f7b43854153044c0a5e9ea2845e07262dcf8 · esnet/iperf · GitHub

サンプルコードの実装と動作確認

以上でなんとなく仕組みがわかったので、簡単なiperf3クライアントをRustで実装してみました。 とりあえず動くこと優先で作ったので、コードが汚いのはご了承ください。

// main.rs

use heapless;
use log::debug;
use num_enum::{FromPrimitive, IntoPrimitive};
use rand::{Rng, RngCore, SeedableRng};
use rand_xorshift;
use serde::{Deserialize, Serialize};

const DEFAULT_TCP_BLOCKSIZE: usize = 128 * 1024;

#[derive(IntoPrimitive, FromPrimitive, Debug)]
#[repr(u8)]
enum IperfApi {
    TestStart = 1,
    TestRunning = 2,
    ResultRequest = 3,
    TestEnd = 4,
    StreamBegin = 5,
    StreamRunning = 6,
    StreamEnd = 7,
    AllStreamsEnd = 8,
    ParamExchange = 9,
    CreateStreams = 10,
    ServerTerminate = 11,
    ClientTerminate = 12,
    ExchangeResults = 13,
    DisplayResults = 14,
    IperfStart = 15,
    IperfDone = 16,
    #[num_enum(default)]
    Unknown,
}

#[derive(Serialize, Deserialize)]
struct IperfConfig {
    tcp: bool,
    omit: i32,
    time: i32,
    parallel: isize,
    /// block size
    len: usize,
    /// pacing timer in microseconds
    pacing_timer: i32,
    client_version: &'static str,
}

impl Default for IperfConfig {
    fn default() -> Self {
        Self {
            tcp: true,
            omit: 0,
            time: 10,
            parallel: 1,
            /// DEFAULT_TCP_BLOCKSIZE
            len: DEFAULT_TCP_BLOCKSIZE,
            /// DEFAULT_PACING_TIMER
            pacing_timer: 1000,
            client_version: "3.8.1",
        }
    }
}

#[derive(Serialize, Deserialize, Default, Debug)]
struct IperfResult<const N: usize> {
    cpu_util_total: f64,
    cpu_util_user: f64,
    cpu_util_system: f64,
    sender_has_retransmits: i32,
    streams: heapless::Vec<IperfStreamResult, N>,
}

#[derive(Serialize, Deserialize, Default, Debug)]
struct IperfStreamResult {
    id: i32,
    bytes: u64,
    retransmits: i32,
    jitter: i32,
    errors: i32,
    packets: i32,
    start_time: f64,
    end_time: f64,
}

use rand::distributions::Alphanumeric;
use std::error::Error;
use std::time::{Duration, SystemTime};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpSocket, TcpStream};

fn make_cookie(cookie: &mut [u8; 37], seed: u64) {
    let mut rng = rand_xorshift::XorShiftRng::seed_from_u64(seed);
    // do not forget '\0' termination
    // TODO: choose from "abcdefghijklmnopqrstuvwxyz234567"
    // ref: make_cookie function
    let (last, c) = cookie.split_last_mut().unwrap();
    c.iter_mut().for_each(|x| *x = rng.sample(Alphanumeric));
    *last = b'\0';
    debug!("cookie: {:?}", String::from_utf8(cookie.to_vec()).unwrap());
}

async fn iperf3_client() -> Result<(), Box<dyn Error>> {
    // Overview
    //  1. c->s Cookie(セッション識別用の文字列)を送る
    //  2. s->c Param exchange(9)を送る
    //  3. c->s 設定の書かれたJSONファイルのサイズを4byteで送る
    //  4. c->s JSONファイルの中身を送る
    //  5. s->c CREATE_STREAMS(10)を送る
    //  6. c->s Cookieを送る
    //  7. s->c TEST_START(1)を送る
    //  8. s->c TEST_RUNNING(2)を送る
    //  9. c->s データを送る
    //  10. c->s TEST_END(4)を送る
    //  11. s->c EXCHANGE_RESULTS(13)を送る
    //  12. c->s JSONのファイルサイズを4byteでおくる
    //  13. c->s 計測結果のJSONファイルを送る
    //  14. s->c JSONのファイルサイズを4byteでおくる
    //  15. s->c 計測結果のJSONファイルを送る
    //  16. c->s IPERF_DONE(16)をおくる

    // Connect to a peer
    let mut command_stream = TcpStream::connect("127.0.0.1:5201").await?;
    let addr = command_stream.local_addr().unwrap();
    println!("command_stream addr = {:?}", addr);

    let seed = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap()
        .as_secs();
    let mut rng = rand_xorshift::XorShiftRng::seed_from_u64(seed);

    // create cookie
    let mut cookie = [0u8; 37];
    make_cookie(&mut cookie, seed);

    command_stream.write(&cookie).await?;

    let mut ack = [0u8; 1];
    command_stream.read(&mut ack).await?;

    match IperfApi::try_from(ack[0]).unwrap() {
        IperfApi::ParamExchange => (),
        _ => unimplemented!(),
    }

    // crate json
    let config = IperfConfig::default();
    let config_str = serde_json_core::to_string::<IperfConfig, 512>(&config)
        .unwrap()
        .to_string();
    let config_str_size = u32::try_from(config_str.len()).unwrap();
    println!("config_str: {:?}", config_str);

    // send json size
    command_stream.write(&config_str_size.to_be_bytes()).await?;
    // send json
    command_stream.write(config_str.as_bytes()).await?;

    // read command
    let mut ack = [0u8; 1];
    command_stream.read(&mut ack).await?;
    match IperfApi::try_from(ack[0]).unwrap() {
        IperfApi::CreateStreams => (),
        _ => unimplemented!(),
    }
    println!("got CreateStreams");

    let mut data_stream = TcpStream::connect("127.0.0.1:5201").await?;

    // resend cookie
    println!("send cookie to data stream");
    data_stream.write(&cookie).await?;

    let addr = data_stream.local_addr().unwrap();
    println!("data stream addr = {:?}", addr);

    // get test start
    println!("waiting to receive TestStart");
    let mut ack = [0u8; 1];
    command_stream.read(&mut ack).await?;
    match IperfApi::try_from(ack[0]).unwrap() {
        IperfApi::TestStart => (),
        _ => unimplemented!(),
    }

    println!("waiting to receive TestRunning");
    let mut ack = [0u8; 1];
    command_stream.read(&mut ack).await?;
    match IperfApi::try_from(ack[0]).unwrap() {
        IperfApi::TestRunning => (),
        _ => unimplemented!(),
    }

    let mut stream_result = IperfStreamResult::default();
    stream_result.id = 1;

    let start_time = SystemTime::now();
    while start_time.elapsed()?.as_secs() < config.time.try_into().unwrap() {
        // send data
        // ref: https://hanya-orz.hatenablog.com/entry/2020/08/05/131158
        let mut data = [0; DEFAULT_TCP_BLOCKSIZE];
        rng.fill_bytes(&mut data);
        let _n = data_stream.write_all(&data).await?;
        // stream_result.bytes += u64::try_from(n).unwrap();
        stream_result.bytes += u64::try_from(DEFAULT_TCP_BLOCKSIZE).unwrap();
    }
    let end_time = SystemTime::now();

    stream_result.end_time = end_time.duration_since(start_time).unwrap().as_secs_f64();

    command_stream.write(&[IperfApi::TestEnd.into()]).await?;

    let mut ack = [0u8; 1];
    command_stream.read(&mut ack).await?;
    match IperfApi::try_from(ack[0]).unwrap() {
        IperfApi::ExchangeResults => (),
        _ => unimplemented!(),
    }
    println!("got ExchangeResults");

    let mut result: IperfResult<1> = IperfResult::default();
    result.streams.push(stream_result).unwrap();
    let result_str = serde_json_core::to_string::<IperfResult<1>, 512>(&result).unwrap();
    let result_str_size = u32::try_from(result_str.len()).unwrap();
    println!("result_str: {:?}", result_str);

    // send json size
    command_stream.write(&result_str_size.to_be_bytes()).await?;
    command_stream.flush().await?;
    // send json
    command_stream.write(result_str.as_bytes()).await?;

    // read result length
    let mut server_result_size_buf = [0u8; 4];
    command_stream.read(&mut server_result_size_buf).await?;
    let mut buf = [0; 512];
    command_stream.read(&mut buf).await?;

    command_stream.write(&[IperfApi::IperfDone.into()]).await?;

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    iperf3_client().await.unwrap();

    Ok(())
}
# Cargo.toml

[package]
name = "iperf3-rs"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
heapless = { version = "0.8.0", features = ["serde"] }
log = { version = "0.4.20", default-features = false }
num_enum = { version = "0.7.1", default-features = false }
rand = { version = "0.8.5", default-features = false }
rand_xorshift = "0.3.0"
# rust-fsm = { version = "0.6.1", default-features = false, features = ["dsl"] }
serde = { version = "1.0.193", default-features = false, features = [
    "derive",
    "serde_derive",
] }
serde-json-core = "0.5.1"
tokio = { version = "1", features = ["full"] }

実行結果

上記コードを実行したところ、とりあえずエラーを出さずテストを完了できました。 結果は大体20Gbps程度でした。

-----------------------------------------------------------
Server listening on 5201
-----------------------------------------------------------
Accepted connection from 127.0.0.1, port 54195
[  5] local 127.0.0.1 port 5201 connected to 127.0.0.1 port 54196
[ ID] Interval           Transfer     Bitrate         Rwnd
[  5]   0.00-1.00   sec  2.27 GBytes  19.5 Gbits/sec  1.22 MBytes
[  5]   1.00-2.00   sec  2.33 GBytes  20.0 Gbits/sec  1.22 MBytes
[  5]   2.00-3.00   sec  2.27 GBytes  19.5 Gbits/sec  2.24 MBytes
[  5]   3.00-4.00   sec  2.28 GBytes  19.6 Gbits/sec  2.24 MBytes
[  5]   4.00-5.00   sec  2.27 GBytes  19.5 Gbits/sec  2.24 MBytes
[  5]   5.00-6.00   sec  2.32 GBytes  20.0 Gbits/sec  2.24 MBytes
[  5]   6.00-7.00   sec  2.30 GBytes  19.8 Gbits/sec  2.24 MBytes
[  5]   7.00-8.00   sec  2.33 GBytes  20.0 Gbits/sec  2.24 MBytes
[  5]   8.00-9.00   sec  2.30 GBytes  19.7 Gbits/sec  2.24 MBytes
[  5]   9.00-10.00  sec  2.33 GBytes  20.0 Gbits/sec  2.24 MBytes
[  5]  10.00-10.00  sec   128 KBytes  9.36 Gbits/sec  2.12 MBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-10.00  sec  23.0 GBytes  19.8 Gbits/sec                  receiver

ちなみに本家iperf3を使うと117Gbps程度出たので、約6倍程度の性能差があります。

~ ❯❯❯ iperf3-darwin -c 127.0.0.1
Connecting to host 127.0.0.1, port 5201
[  5] local 127.0.0.1 port 54220 connected to 127.0.0.1 port 5201
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd          RTT
[  5]   0.00-1.00   sec  13.3 GBytes   114 Gbits/sec    0   4.00 MBytes   1ms
[  5]   1.00-2.00   sec  13.7 GBytes   117 Gbits/sec    0   4.00 MBytes   1ms
[  5]   2.00-3.00   sec  13.7 GBytes   118 Gbits/sec    0   4.00 MBytes   1ms
[  5]   3.00-4.00   sec  13.6 GBytes   117 Gbits/sec    0   4.00 MBytes   1ms
[  5]   4.00-5.00   sec  13.5 GBytes   116 Gbits/sec    0   4.00 MBytes   1ms
[  5]   5.00-6.00   sec  13.6 GBytes   117 Gbits/sec    0   4.00 MBytes   1ms
[  5]   6.00-7.00   sec  13.7 GBytes   117 Gbits/sec    0   4.00 MBytes   1ms
[  5]   7.00-8.00   sec  13.7 GBytes   117 Gbits/sec    0   4.00 MBytes   1ms
[  5]   8.00-9.00   sec  13.5 GBytes   116 Gbits/sec    0   4.00 MBytes   1ms
[  5]   9.00-10.00  sec  13.7 GBytes   117 Gbits/sec    0   4.00 MBytes   1ms
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec   136 GBytes   117 Gbits/sec    0             sender
[  5]   0.00-10.00  sec   136 GBytes   117 Gbits/sec                  receiver

iperf Done.

もうちょっとiperfの設計や実装を理解して真似すると速くなるのかもしれません。

以上です。

ディスク復号キーに使うPCR Bankはどれを選べばよいかの調査メモ

PCにTPMがついている場合、TPM内の値を復号キーとして用いてストレージの暗号化解除を行えます。

私の家では以下の記事を参考に、clevisを用いてUbuntuを入れたサーバー機すべてのストレージをdm-cryptで暗号化し、起動時はTPMを使って自動的に暗号化解除するように設定しています。 takuya-1st.hatenablog.jp

これによりストレージを暗号化しつつ、面倒な起動時の復号キー入力を省けるので便利です。

ディスク暗号化の解除はTPM内のPCR(Platform Configuration Registersの略、詳細はTPMの仕様書, pp.50, 11.6.2 Platform Configuration Registers (PCR)参照)の特定Bankに記録された値を用いて行います。

このPCRは複数のBankがあり、コンピュータへ何らかの変更が行われると、対応するPCR Bankの値も変更されます。 (PCRの計算式はTPMの仕様書, pp.81, 17.3 Using Extend with PCR Banksあたりを参照)

そのため、暗号化解除に利用するPCR Bankを適切に設定することで、PCに行われた変更を検出してストレージを復号不可にできます。 例えば、起動順序を変更してUSBメモリから起動するようにしてディスクの中身を読もうとしても、PCR Bankを適切に設定すれば起動順序変更時にPCRの値が変わるためディスク暗号化を解除できません。

PCRの各Bankは何をすると変更されるか

PCRの各Bankの使われ方についてはTCG PC Client Platform Firmware Profile Specification という資料の「3.3.4 PCR Usage」にかかれています。

TCG PC Client Platform Firmware Profile Specification, pp.26, 3.3.4 PCR Usage, Table 1 PCR Usageより概要の表を引用します。

また、3.3.4小節には何が起きたときに各Bankの値が変更されるかの仕様が書かれています。 そのため、ここを読めば「どのPCR Bankを復号キーに設定すると、どんな変更を検知して暗号化解除不可にできるか」がわかります。

例えばPCR Bank 7を復号キーに設定すると、セキュアブート設定を変更された場合にディスクの暗号化解除不可にできます。

どのPCR Bankを復号キーに使えば良いのか

「PCがまるごと盗まれた」というシナリオで「アカウントのパスワードが分からなければディスクの中身を見られない」ようにしたい場合、どのPCR Bankを復号キーに設定すればよいか考えてみます(復号キーの安全性は十分高いものとする)。

私が簡単に考えた攻撃シナリオは以下の2つです。

  • ディスクを取り出して別のPCからディスクを読む
  • 元々のPC上にUSBメモリを差し込み、別のOSを起動してディスクを読む

ディスクを取り出して別のPCからディスクを読む

この方法は復号キーの入ったTPMが別のPCに無いので、ディスク暗号化解除ができません。 なのでこの攻撃を防ぎたい場合、どのPCR Bankを使っても良いと思います。

元々のPC上にUSBメモリを差し込み、別のOSを起動してディスクを読む

USBメモリから別のOSを起動すると、元々のPCについているTPMを使ってディスク暗号化解除を行えないでしょうか。

起動順序が変更されると、PCR Bank 4(とBank 1)が変更されます。 これは tpm2_pcrreadコマンド を実行して、起動順序変更前後の差分を見ると確認できます。

よって、PCR Bank 4を復号キーにセットすれば、別のOSを起動してディスク暗号化解除を防げるはずです。

結局どのBankを使えばよいの?

私はPCが盗まれたときに暗号化解除されなければ良いので、Bank 4をセットすると良いかなと考えています。

おまけ

設定変更後にもとに戻すとでPCRの値ももとに戻るのはなぜ?

PCR Bank4の変化を見ていて、自分の理解と異なる動作をしたのでSNSで相談したところ、あきひろさんが理由を教えて下さいました。

自分でも調べてみると、PCRTPMのRAM上にあり電源喪失時にデータが保持されるかは実装依存(基本喪失する)と書かれています。

Random access memory (RAM) holds TPM transient data. Data in TPM RAM is allowed, but not required, to be lost when TPM power is removed. Because the values in TPM RAM may be lost, in this specification they are referred to as being volatile, even if the data loss is implementation-dependent. TPMの仕様書, pp.50, 11.6.1 Introduction) より引用

そのため、まきひろさんのご指摘どおり、初期値が同じで記録する構成がもとに戻れば、変更前と変更後にもとに戻した場合のPCR値は一致するという理解をしました。

参考資料

https://www.rcis.aist.go.jp/files/events/2008/RCIS2008/RCIS2008_3-2_Suzaki.pdf

CONFIG_ADDRESSの出どころをしらべてみた

最近はx86のお勉強をしようと考えて、uchanさんの「ゼロからのOS自作入門(通称みかんOS本)」を読んでいます。 私はx86のことをほとんど知らないので、armとの違いを感じられて読んでいて楽しいです。

さて、先日6章3節「PCIバイスの探索」を読み進めていたところ、少し興味深い内容がありました。 その記述内容を引用します。

PCIコンフィグレーション空間を読むには CONFIG_ADDRESS レジスタと CONFIG_DATA レジスタを使います。 それぞれIOアドレス空間の 0x0cf8 と 0x0cfc にある32ビットレジスタです (ゼロからのOS自作入門,pp.142より引用)

「ここで紹介されているレジスタアドレスの出どころ(資料)はどこなんだろう」と気になったのですが、みかんOS本だけでなくいくつかのサイトにも記述が見つからなかったので、詳しく調べてみることにしました。 (調べたサイトたち)

x86初心者なので、もっと良い資料や説明があれば教えていただけると嬉しいです 🙏

調査

x86 pci 0x0cf8」などで検索していたところ、手がかりになりそうな内容を記載しているページを見つけました。 当該部分を引用します。

CPU I/Oポートの0xCF8(固定)と0xCFC(固定)を使ってPCIバイスコンフィギュレーション空間にアクセスし、PCI互換レジスタを設定する(この方法はチップセット実装に依存する)。 (PCI/PCI Express バスについて調べてわかったことなど | DXR165の備忘録 より引用)

チップセットとは何かを調べたところ、チップセット ‐ 通信用語の基礎知識 にわかりやすい説明が書かれていました。

こちらに書かれていた内容を簡単にまとめると、少し前までのx86プロセッサは周辺回路・IOコントローラーをCPUの外のチップ(ノースブリッジ・サウスブリッジ)に任せていたようです。

以下のwikipediaの図もチップセットの役割の理解の助けになりました。

Motherboard diagram.svg By Original: Gribeco at French Wikipedia Derivative work: Moxfyre at English Wikipedia - This file was derived from: Diagramme carte mère.png Block diagram of a late-2000s motherboard (legend in English). CC BY-SA 3.0

話を戻すと、チップセットまたはCPUの仕様書を探すと CONFIG_ADDRESSレジスタ等のアドレスを見つけられそうです。

そこでチップセットを検索ワードに入れて調べてみたところ、E8500というノースブリッジのデータシートを見つけました。

https://www.intel.co.jp/content/dam/doc/datasheet/e8500-chipset-north-bridge-datasheet.pdf

このデータシート内で検索をかけたところ、探していたCONFIG_ADDRESSが記載されていました。 当該部分を引用します。

The PCI configuration access mechanism enables support of legacy/PCI code that utilizes the PCI mechanism. (省略) The Intel® E8500 chipset has reserved two special I/O locations (0xCF8 / 0xCFC) for direct configuration accesses. (Intel® E8500 Chipset North Bridge (NB), 4.6 I/O Mapped Registers, pp.56より引用)

以上より「CONFIG_ADDRESS の定義は(少なくとも)チップセットのデータシートに書かれている」という結果が一旦得られました。

さらに気になるけど調べられていないこと

チップセット ‐ 通信用語の基礎知識 の内容を読むと、最近のx86プロセッサはチップセットがプロセッサチップ側に吸収されているとのことです。 「つまり最近のx86 CPUのデータシート等を読めば CONFIG_ADDRESS が書かれているのでは?」と考えて軽く調べてみましたが、自分ではCPU側のデータシートに記載されている例を見つけられませんでした。 もしこのあたりの事情をご存じの方がいれば、教えていただけると嬉しいです。

VyOS 1.4で特定デバイスだけPPPoEでアクセスできるようにする

背景

技術的な興味からIPoE + ds-liteの接続を普段づかいように残しつつ、特定機器だけPPPoE接続にする方法を調査して設定してみました。 その方法を備忘録として残しておきます。

環境

  • マシン
    • Shuttle DS68U
      • CPU: Intel(R) Celeron(R) CPU 3855U @ 1.60GHz
      • Memory: 8GB
      • Storage: 120GB
  • VyOS
    • 1.4-rolling-202212310809

やること

主に以下のページを参考にして進めました。

yosida95.com

大まかには以下を行えば良いようです。

  • 特定デバイスをpolicy tableに登録する
  • table単位でstatic routeの設定をしてpppoe経由でインターネットへ繋がるようにする

前提として、pppoeの設定は済んでおり、pppoe0として見える状態になっているものとします。

1. ipアドレスを固定する

今回はIPアドレス指定でtableを作りたかったので、DHCPIPアドレスを固定することにしました。

特定デバイスMACアドレスを調べ、dhcp static mappingでIPアドレスを固定します。

docs.vyos.io

set service dhcp-server shared-network-name LAN subnet 192.168.XXX.0/24 static-mapping デバイス名 ip-address '192.168.XXX.100'
set service dhcp-server shared-network-name LAN subnet 192.168.XXX.0/24 static-mapping デバイス名 mac-address 'デバイスのMACアドレス'

2. policyをつくる

static route設定を適応するデバイスを入れたテーブルを作ります。

set policy route game interface 'デバイスの接続しているインターフェース'
set policy route game rule 1 set table '1'
set policy route game rule 1 source address '192.168.XXX.100/32'

3. pppoeへのstatic route設定をする

policy部で作ったtableに対してstatic routeの設定を入れます。

set protocols static table 1 route 0.0.0.0/0 interface pppoe0

4. commitして様子を見る

これで commit すれば特定デバイスの通信だけがpppoe経由で行われるようになるはずです。

うまく設定ができていれば特定デバイスグローバルIPアドレスが設定前から変わっているはずです。

参考サイト

VyOS 1.4にAdGuardHomeを入れる

セキュリティ強化とトラッキング対策を目的としてVyOSにAdGuardHomeを入れてみたので、備忘録を残しておきます。

環境

  • マシン
    • Shuttle DS68U
      • CPU: Intel(R) Celeron(R) CPU 3855U @ 1.60GHz
      • Memory: 8GB
      • Storage: 120GB
  • VyOS
    • 1.4-rolling-202212310809

やること

VyOS 1.4からはcontainer機能が追加されています。 これを用いるとVyOSの環境を汚さずにVyOSの機能追加ができます。

docs.vyos.io

本記事の前提として、vyosのインストール及びルーターとして機能するまでの初期設定は済んでいるものとします。

1. コンテナの追加

adguardhomeのコンテナをVyOSに追加します。

add container image adguard/adguardhome

2. 設定ファイル用ディレクトリの作成

adguardの設定ファイルを入れるためのディレクトリを作ります。

vyosにはos imageのupdateで消えるところと消えないところがあるので、updateしても消えない /config に保存するようにしています。

sudo mkdir -p /config/opt/adguard/{conf,work}

3. adguard用設定の追加

以下のコマンドを実行してcontainerの設定を追加します。 172.20 の部分はLANのNWとかぶらないように適宜変更してください。

set container name adguard image 'adguard/adguardhome'
set container name adguard network adguard-net address '172.20.0.10'
set container name adguard volume adguard_conf destination '/opt/adguardhome/conf'
set container name adguard volume adguard_conf source '/config/opt/adguard/conf'
set container name adguard volume adguard_work destination '/opt/adguardhome/work'
set container name adguard volume adguard_work source '/config/opt/adguard/work'
set container network adguard-net prefix '172.20.0.0/24'

このコマンド実行後に commit して、adguardhomeを起動してください。

4. adguardhomeの初期設定をする

adguardhomeはvyosの内部NW(172.20.0.10)に建っているので、通常vyosの外からアクセスできません。 そのため、ssh port forward機能を使ってアクセス経路を作り、初期セットアップを行います。

vyosにSSHログインできる端末から以下を実行してください。

 ssh vyos@VYOSのIPアドレス -L 13000:172.20.0.10:3000

ターミナルで接続を維持したまま、ブラウザで http://localhost:13000 を開くとadguardhomeの初回セットアップが開始されます。 セットアップ方法は以下を参考にして行ってください。

github.com

セットアップ後、以下のコマンドを実行してdnsが引けるか確認してください。

vyos@vyos:~$ dig @172.20.0.10 google.co.jp

; <<>> DiG 9.16.33-Debian <<>> @172.20.0.10 google.co.jp
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 38651
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;google.co.jp.          IN  A

;; ANSWER SECTION:
google.co.jp.       300 IN  A   172.217.161.35

;; Query time: 159 msec
;; SERVER: 172.20.0.10#53(172.20.0.10)
;; WHEN: Thu Jan 05 09:03:12 UTC 2023
;; MSG SIZE  rcvd: 57

ipv6のNWも組んでいる人はAAAAレコードも引けるか確認するのがおすすめです。

vyos@vyos:~$ dig @172.20.0.10 google.co.jp aaaa

; <<>> DiG 9.16.33-Debian <<>> @172.20.0.10 google.co.jp aaaa
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62224
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;google.co.jp.          IN  AAAA

;; ANSWER SECTION:
google.co.jp.       109 IN  AAAA    2404:6800:4004:80a::2003

;; Query time: 9 msec
;; SERVER: 172.20.0.10#53(172.20.0.10)
;; WHEN: Thu Jan 05 09:04:05 UTC 2023
;; MSG SIZE  rcvd: 69

5. vyosのdns forwardで使うDNS serverにadguardhomeを指定する

以下のコマンドを実行して、forwardするDNSサーバーをadguardhomeのものに変更します。

set service dns forwarding name-server '172.20.0.10'

commit 後、LAN側でdigを実行するなどしてdnsが引けるのを確認してください。

(おまけ) 6. adguardhomeの管理画面を開く

初回セットアップ後、port 80番にアクセスするとadguardhomeの管理画面に接続できます。 この際、初回セットアップ時と同様にport forwardが必要です。

 ssh vyos@VYOSのIPアドレス -L 18080:172.20.0.10:80

コマンド実行後 http://localhost:18080 にアクセスすると管理画面が開けます。

(おまけ) 7. マルウェアブロックできる上流DNSサーバーに変更する

上流DNSサーバーを変更することで悪性ドメインへのアクセスを防ごうと思います。 マルウェア対策が行われている公開dnsサーバーはいくつかありますが、以下の理由でcloudflareのDNSサーバーを利用することにしました。

  • AAAAレコードが引ける
  • DNS over HTTPS(DoH)が使える

developers.cloudflare.com

adguardhomeの管理画面に入り、上流DNSサーバーを https://security.cloudflare-dns.com/dns-query に設定してください。

設定を保存して反映し、LAN側のマシンでdig等を使いAレコードとAAAAレコードが引けるのを確認して、問題なければ変更完了です。

adguardhome導入その後

ここまでセットアップしてしばらく使用してみたところ、主にスマフォアプリがNWエラーで使えなくなる問題が多発したので、結局アドブロックとトラッキングブロック機能をOFFにすることにしました。

今はDoHをDNSに変換したり、接続先を可視化するレイヤーとしてadguardhomeを利用しています。

参考サイト