dullwhaleのメモ帳

何度も同じことを調べなくてよいように...

LinuxでHDDに対する書き込みテストとデータ消去

前提

ここに書いているのはあくまでHDDにだけ通用する内容であって、SSDでは異なるアプローチが必要であることに注意する。

HDDに対する書き込みテスト

HDDの大まかな情報はS.M.A.R.T.を読み取れば分かるが、全セクタにわたって本当に問題がないのか疑わしいときは、実際に全セクタへbyte列を書き込み、読み取りテストしてみなければ分からない。 そのようなテストに、Linuxではbadblocksコマンドが使える。

badblocksはbyte列を実際にストレージに書き込んでから読み出すことでセクタが壊れていないか確認する。 この原理のため、現代の大容量のHDDではとてつもない時間がかかることには注意が必要だ。

次は書き込みモード-wで/dev/sdaをテストする。

sudo badblocks -b 4096 -wsv -o badblocks.txt /dev/sda

書き込みモードでは対象のデバイスにデータがあろうが破壊的にbyte列を書き込むので注意

-b 4096でブロックサイズサイズ(セクタのサイズ)が4096 byteであることを伝え、少しでも高速化を期待している。 このブロックサイズはHDDに固有だから個別に調べる必要がある。 -sは進捗を表示させ、-vはverboseに出力させる。 -o badblocks.txtは発見した不良セクタの番号をbadblocks.txtに書き込むことを指示している。

さらに、ここでは書き込むbyte列を指定していないからデフォルトの動作になる。 デフォルトの動作では以下のようにテストが進められる。

  1. 0xaa(0b10101010)を書き込む
  2. 読み出して0xaa(0b10101010)と一致するか確認する
  3. 0x55(0b01010101)を書き込む
  4. 読み出して0x55(0b01010101)と一致するか確認する
  5. 0xff(0b11111111)を書き込む
  6. 読み出して0xff(0b11111111)と一致するか確認する
  7. 0x00(0b00000000)を書き込む
  8. 読み出して0x00(0b00000000)と一致するか確認する

USB 2.0接続の500 GBのポータブルHDDを上記コマンドでテストしたところ、全て完了するまでに27時間強がかかった。

HDDのデータ消去

HDDを処分する際のデータ消去方法として、Linuxではshredコマンドとddコマンドを用いる方法をよく見かける。 ここではshredコマンドを使った方法について説明する。

shredコマンドはファイルシステム上の単一のファイルに対して実行することもできるが、ブロックデバイスに対して実行することもできる。 次は/dev/sdaを2回-n 2ランダムなbyteデータで書き込み、最後に0-zで埋める。

sudo shred -n 2 -vz

-vはverboseを表し、進捗を表示する。

新しいHDDは一回の書き込みで十分だとされているから、そのようなものは0埋めだけで十分かもしれない。

Raspberry Pi 3 Model BにDebian 13ベースRaspberry Pi OS 64bitをインストール

久々にRaspberry PiのOSインストールと初期セットアップを行おうとしたら以前の記事を書いたときから微妙な変化があった。

前提

  • サーバとして運用するからGUI環境は不要
  • ネットワークにはEthernet接続しかしない
  • インストール先のメディアは16 GBのSDカード

OSイメージの入手からインストールまで

Raspberry Pi OSの公式ページからダウンロードする。 Raspberry Pi OS (64-bit)のRaspberry Pi OS LiteにあるDownloadボタンを押す。 XZ圧縮されたOSイメージがダウンロードされる。 ダウンロード時点では次のファイル名だった。

2025-12-04-raspios-trixie-arm64-lite.img.xz

イメージをSDカードに書き込む際のツールとしてRufusをつかった。 上記のXZ圧縮されたイメージを展開することなく直接指定して良い。

初回起動前の設定

消費電力削減とセキュリティ向上のため、不要な機能をOFFにする。 まずは先ほどOSイメージを書き込んだSDカードのFAT32パーティション領域をマウントする。 マウントしたディレクトリ直下にconfig.txtが存在することを確認する。

SSHデーモンの有効化

ディスプレイやキーボード、マウスをいちいち付外ししたくないからSSH接続できるようにしたい。 Raspberry Pi公式の説明によると sshファイルかssh.txtファイルがあるとブート時にSSHサーバが有効化されるようだ。

ssh or ssh.txt

When this file is present, enables SSH at boot. SSH is otherwise disabled by default. The contents do not matter. Even an empty file enables SSH.

ここではマウントしたディレクトリ直下に空のsshファイルを作成した。

不要な機能を無効化する

消費電力削減とセキュリティ向上のため、不要な機能をOFFにする。 ディレクトリ直下のconfig.txtを開き以下の設定を追加する。

# オンボードのサウンドカードをOFFにする。
dtparam=audio=off
# 有線接続しかしないから、BluetoothをOFFにする。
dtoverlay=disable-bt
# 有線接続しかしないから、Wi-FiをOFFにする。
dtoverlay=disable-wifi

詳しい設定内容はconfig.txtのドキュメントを参照せよ。 dtoverlayで設定するものについてはGitHubのfirmwareのREADMEを参照しないといけない場合がある。

ユーザを作成する

以前はデフォルトユーザとしてユーザ名pi、パスワードraspberryが作成されていたが、現在は作られなくなった。 headlessインストールではこの仕様は困る。 ただし、ドキュメントによるとuserconf.txtを作成し、そこに1行のユーザ情報を記載しておけば作成してくれる。

This file should contain a single line of text, consisting of <username>:<password>: your desired username, followed immediately by a colon, followed immediately by an encrypted representation of the password you want to use.

これは/etc/shadowの1行分を記載する必要がある。 必然的に<password>の部分はハッシュ化されたものを書き込む必要がある。 そのパスワードハッシュはopensslコマンドで次のようにして作成できる。

# ソルトをtest、パスワードをtestとするSHA-512ハッシュの生成
$openssl passwd -6 -salt 'test' 'test'
$6$test$s73HObLPm3spffEErq3RSoFse8b43m.tVitc.rEapP4nbLqdmZZabQpuXHItK6ZdlYGqOs5nbhFsWb.ZHZlYa0

<username>userとした場合、最終的にuserconf.txtの中身は次のようになる。

user:$6$test$s73HObLPm3spffEErq3RSoFse8b43m.tVitc.rEapP4nbLqdmZZabQpuXHItK6ZdlYGqOs5nbhFsWb.ZHZlYa0

ソルト長はNISTガイドラインで少なくとも32 bitとされているから、4文字以上が望ましい

初回起動と最低限の初期設定

LANケーブルを繋ぎ、SDカードを挿した状態で電源を供給する。 しばらく待つとDHCPサーバ側でIPリースが行われたことが分かるから、次のようにしてラズパイにSSHする。

ssh ${前節で作成したユーザ}@${リースされたIPアドレス}

SSH後の初期設定

raspi-configを使ってTUIで各種設定する。

sudo raspi-config

後から変更できるものが多いため、そこまで慎重にならなくてよい。

  • Interface Options
    • SSHサーバを有効化
  • Localisation Options
    • ロケールをja_JP.UTF-8に変更
    • タイムゾーンをTokyoに変更

パッケージの調整

vimをインストール

sudo apt install vim-nox

不要パッケージをまとめてアンインストールする。 ここでは特に、不要なデーモンを停止してRAMの使用量を少しでも削減することに重きを置いている。

sudo apt purge alsa-utils avahi-daemon bluez modemmanager nano wpasupplicant
sudo apt autoremove

それぞれのパッケージが不要な理由

  • alsa-utils オーディオを使わないから
  • avahi-daemon mDNSを使わないから
  • bluez Bluetoothを使わないから
  • modemmanager モバイル回線経由でのネットワーク接続などしないから
  • nano 代わりにvimを使うから
  • wpasupplicant Wi-Fiを使わないから

IPアドレスの固定化

NetworkManagerと対話することで固定のIPアドレスを設定できる。 ここでは少ない操作で設定可能なnmcliを使う。 例えばGWのIPv4アドレスを192.168.1.254/24、自ホストの固定IPアドレスを192.168.1.1/24の場合なら次のように設定すればよい。

sudo nmcli con modify 'Wired connection 1' ipv4.method manual ipv4.addresses 192.168.1.1/24 ipv4.gateway 192.168.1.254 connection.autoconnect yes

modifyの次の接続の識別子はUUIDでの指定もできる。 そのUUIDは次のコマンドで確認できる。

nmcli connection

以前はdhcpcd.confファイルを変更して修正していたが、Debian 13では機能しないことに注意せよ。

再起動と固定したIPアドレスでのSSH再接続

sudo reboot

してしばらく待ち、固定後のアドレスでsshしてみる。 IPが変わっているためssh時にman-in-the-middle攻撃を受けているかも警告が出ることがある。 ~/.ssh/known_hostsから該当する行を削除すれば接続できる。

組み込み向けの軽量SSH実装であるDropbearを使う際の設定

組み込みシステムやネットワーク機器など、極端に計算機資源が限られる環境ではOpenSSHに代わってDropbearというSSH実装が用いられることがある。

よく、OpenSSHを代替するかのように説明されるが、ネットワークプロトコルとしての互換性はともかく、設定方法やコマンドは互換性が無いものも多いことに注意する。 この記事は、そのような互換性が無い挙動について、自分のユースケースの観点から問題になる部分についてメモしている。

Dropbearには設定ファイルが無い

OpenSSHは設定ファイルを用いてSSHクライアント/サーバの挙動を制御できるが、Dropbearには設定ファイルが無い。 Dropbearは全ての挙動をコマンドラインオプションを用いて制御する。

Dropbearでは鍵ペアを生成するコマンドとファイル形式が異なる

秘密鍵のファイルフォーマットの違い

OpenSSHでは普通、ssh-keygenコマンドを使って鍵ペアを生成する。

一方Dropbearではdropbearkeyというコマンドを使って鍵ペアを生成する。 更に重要なのは秘密鍵のファイルフォーマットの互換性がないことだ。 この違いは単純にファイルを標準出力するだけでも確認できる。

OpenSSHの秘密鍵は次のような形式になっているが

-----BEGIN OPENSSH PRIVATE KEY-----
BASE64エンコードされているバイナリ
-----END OPENSSH PRIVATE KEY-----

Dropbearの秘密鍵は次のような形式になっている。

ssh-${アルゴリズム}@${文字化けするような生のバイナリ列}

だからssh-keygenコマンドで生成された秘密鍵のファイルをそのままDropbearで使うことはできないだろう。

dropbearkeyコマンドを用いてDropbearが認識できる秘密鍵を生成する

dropbearkeyを用いてed25519鍵ペアを生成するには次のようにすれば良い。

# ed25519アルゴリズムで秘密鍵を~/.ssh/id_dropbear、公開鍵を~/.ssh/id_dropbear.pubとして鍵ペアを生成する
dropbearkey -t ed25519 -f ~/.ssh/id_dropbear

ssh-keygenと異なり、-fオプションを用いて明示的に秘密鍵のファイル名を指定する必要があることに注意する。 また、そのファイルパスは同コマンドのヘルプテキストに書かれているように、特別な理由がないなら次のパスを指定することが望ましい。

~/.ssh/id_dropbear

-f filename Use filename for the secret key. ~/.ssh/id_dropbear is recommended for client keys.

GnuCashの勘定科目コード設計メモ

GnuCashにいくつかあるインポート機能の中に、CSVを用いた取引のインポートがある。 このCSVインポート時に、安全にしたいなら勘定科目コードをCSV中に記載する。 ただし、そのためには先に既存の勘定科目にコードを設定しておく必要がある。

この記事では、勘定科目コードの決め方についてGnuCashの推奨事項を考慮しつつ、自分にとって扱いやすいコードとなるよう設計する大枠の方針を残しておく。

設計方針

  • 勘定科目科目コードに使う文字集合は[0-9a-z]
  • 葉/末端の勘定科目コードは最下位の文字が非ゼロ
  • 枝/親の勘定科目コードは最下位が0
  • 勘定科目コードの桁数は最大の深さを持つ勘定科目に合わせて決める
  • 雑益・雑損のような「その他」の性質を持つ勘定科目のコードは最下位の文字をzにしておく。

CSVインポート時の手順と注意

CSVを用いた取引のインポート時、いくつかの列指定の方法がある。 ここでは元となるCSVファイルが作成しやすい「総勘定元帳」のような形式のものを想定する。

  1. メニューバーから「ファイル(F)」>「インポート(I)」 >「CSVから取引をインポート(T)...」をクリックする。
  2. ウィザードのウィンドウが開くから、読み込むファイルを選択した直後まで進める。
  3. ウィンドウ上部の「勘定科目」プルダウンでベースとなる つまり総勘定元帳の勘定科目を選択する。
  4. CSVファイルの中身がプレビューされている下部の表示では、反対の勘定科目コードを「資金移動先勘定科目」として指定する。
  5. 残りの必須の列「日付」、「金額(符号反転)/金額」、「説明」の列を指定する。
  6. 「進む(N)」ボタンでCSVファイルを処理させ、必要なら除外や調整をしてインポートをcommitする。

設計の根拠となるGnuCashの仕様と推奨事項

GnuCashに付属のマニュアル中の「Creating a Chart of Accounts」節には以下の記載がある。

ここで、英文中のaccountが異なる意味で使われていることに注意せよ。 1つ目の文中のaccountsは勘定科目を意味し、2つ目の文中のaccountsは銀行口座を意味する。

The best way to conceptualize a chart of accounts is as a tree. The main branches represent entire categories or groups, while the leaves of the tree denote individual bank accounts or expense categories.

ようは木構造となるようにコードを設計すべきだと言っている。 そして、より具体的なコードの付け方として以下の記載がある。

It’s customary to have the leaf accounts end in non-zero digits, while parent nodes have increasing numbers of zeros.

葉の勘定科目は非ゼロの数字で終わり、その親の勘定科目は末端が0のものが増えていくようにするのが慣例だと言っている。 例えば以下のように決めるのが慣例だと言っている。

300          費用
|
+-- 310      新聞図書費
|    |
|    +-- 311 書籍代
|
+-- 320      水道光熱費
|    |
|    +-- 321 ガス代
|    |
|    +-- 322 水道代

そして最後に重要な仕様と制限について記載がある。

GnuCash does not prevent duplicate numbering, although we would encourage you to avoid this. Account codes are treated as numbers in base-36, thus, if you run out of numbers, you can use the letters, a through z.

なんとGnuCashは勘定科目コードの重複を禁止しない。 人間が気を付けて採番する必要がある。

勘定科目コードはBASE 36として扱われるから、葉の勘定科目が増えて数字が足りなくなったらaからzの文字を使うことができる。 別の言い方をすると、BASE 36で定義されている文字集合しか使えない。

高速な非暗号学的ハッシュアルゴリズムxxHashを使う

ハッシュの利用目的が改ざんの検知などでは「ない」とき、定番のアルゴリズムであるSHA(secure hash algorithm)やMD5(message digest algorithm 5)は不適なことがある。 そのような場合にxxHashアルゴリズムの利用を検討すると良い。

非暗号学的な利用においては、ハッシュ値の計算速度が重要な場合がある。 既に脆弱と言われているMD5SHA1ですら、暗号学的ハッシュとして設計されているから、その用途の観点で見ると無駄な演算が発生し遅い。

非暗号学的用途に特化して設計されていて、ある程度普及しているハッシュアルゴリズムとしてxxHashがある。 オリジナルはC言語で実装されているようだが、主要な言語向けの移植が揃っている。

cf. xxHash - Extremely fast non-cryptographic hash algorithm

Androidの通知は属性をきちんと設定しないとPendingIntentの挙動がおかしくなる

通知やPendingIntentに関する一般的なミスを一通り確認した後でも原因不明なら、通知を作成する際にアイコンを設定しているか確認せよ。

Android 11において、通知をタップした際のPendingIntentでActivityを開くようコードを書いていたが、代わりにAndroid OS側のアプリの設定画面が開いてしまう挙動に悩まされていた。 原因は通知のアイコンを設定していないことだった。 一見無関係に思えるが、setSmallIcon()でアイコンを指定しただけで想定通りにPendingIntentが動くようになった。

以下に試行錯誤していた際のログを残す。

問題のコードと原因特定

次は、タップしたらMainActivityを開く通知を設定するコードスニペットである。

// スニペットにimportが書かれていないことが多くて困るから記載している
import android.app.PendingIntent
import android.content.Intent
import androidx.core.app.NotificationCompat

// ===== 中略 =====

val launchIntent = Intent(this, MainActivity::class.java)
val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, launchIntent, PendingIntent.FLAG_IMMUTABLE)
// このchannelIdは別のところで作ってる。本筋とは無関係だから省略。
val notification = NotificationCompat.Builder(this, channelId) 
// このsetSmallIcon()が重要。設定しさえすればよいから、
// プロジェクト作成時に自動配置されるアイコンを指定している。
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentIntent(pendingIntent)
.build()

コードの記述の意図はsetContentIntent()で指定したPendingIntentが通知タップ時に発火し、MainActivityが開かれることである。 このままなら、その意図通りの挙動となる。 ところがsetSmallIcon()の行を消してしまうと、通知タップ時にMainActivityではなくAndroid OS側のアプリ設定画面が開かれてしまう。

setContentTitle()setContentText()を設定しても問題は解消しなかった。 setSmallIcon()をセットしなければ解消しないようだ。

問題が再現した環境情報

Wi-Fiからすぐに切断されてしまうAndroid端末の調査と妥協的解決策

自宅LAN内において、少し古いAndroid端末が数分毎にWi-Fiから切断される問題があった。 この記事ではADB(android debug bridge)を用いたデバッグログとWi-Fiルーター/AP(access point)の設定変更で問題の原因の絞り込みと、回避するための構成について説明する。 検証では真の原因を特定できなかったが、次の対応策が有効だった。

Androidバージョン11端末が接続するSSID(service set identifier)においては、IPv6を無効化した上でWPA(wi-fi protected access)3を使わない。

発生していた問題

少し古いAndroidバージョン 11の端末が数分毎にWi-FiSSIDとの接続で解除される。

調査方法と試行錯誤のログ

ADBを用いてPCでログを確認するための準備

最初にPCでADBが使えることを確認する。 ADBはAndroid Studioに同梱されているが、「SDK Platform-Tools」として、ツール群単体でのダウンロードも可能である。 詳細は次のリンクを参照せよ。

SDK Platform Tools release notes  |  Android Studio  |  Android Developers

次に、Android端末を操作し「開発者向けオプション」を有効化した上で、「USBデバッグ」を有効化する。 詳細な手順はAndroid公式のドキュメントを参照せよ。

PCと調査対象のAndroid端末をUSBケーブルで接続して、Android側でUSBデバッグを許可する。

次のようにlogcatサブコマンドでログが出力されることを確認する。

./adb logcat 

IPv6有効時のIpReachabilityMonitorのバグ発見と対処

ADBのlogcatで、Wi-Fiが切断される直前に次のようなログが出力されていることを発見した。 なお、MACアドレスIPアドレスなどの情報は*などでマスクしている。

SSIDでのIPv6が有効な状態においてWi-Fi接続が切れる直前にlogcatで出力された内容

01-01 21:54:07.286  2015 27567 W IpClient.wlan0: [IpReachabilityMonitor] WARN ALERT neighbor went from: NeighborEvent{@124682102,RTM_NEWNEIGH,if=32,*.*.*.*,NUD_PROBE,[**:**:**:**:**:**]} to: NeighborEvent{@124692083,RTM_NEWNEIGH,if=32,*.*.*.*,NUD_FAILED,[null]}
01-01 21:54:07.288  2015 27567 W IpReachabilityMonitor: FAILURE: LOST_PROVISIONING, NeighborEvent{@124692083,RTM_NEWNEIGH,if=32,*.*.*.*,NUD_FAILED,[null]}, NUD event type: NUD_ORGANIC_FAILED_CRITICAL
01-01 21:54:07.310 27565 27565 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid=**:**:**:**:**:** reason=3 locally_generated=1

この問題はIssueTrackerで報告されている。 いまいち的を得た回答が返されていないが、どうやらAndroidのOSの一要素であるIpReachabilityMonitorのバグが原因のようだ(確信なし)。 IpReachabilityMonitorはネットワーク的に接続性が維持されているか否か確認するモジュールのようだ。 IpReachabilityMonitorゲートウェイ(GW: gateway)に向かって定期的にneighbor discoveryを行い、レスポンスを一定時間以内に受信できない場合は現在のWi-Fi接続を切断するようだ。 上のログではマスクしているが、IPv4アドレスに対してIPv6のneighbor discoveryを試みているかのようなログ内容が大変怪しい。

Google Issue Tracker

ワークアラウンドとして、SSIDIPv6を無効化し、IPv4のみで通信させるよう設定」したところ、上記の理由で切断されることは無くなった。

古いバージョンのwpa_supplicantがWPA3を理解できない問題の発見と対処

上記の設定でIpReachabilityMonitorによりWi-Fi接続が切断されることは無くなったが、数分毎に切断される事象自体は解消していない。 引き続きlogcatでログを監視し、切断直前に次の内容が表示されていることを発見した。

WPA3-Personalが有効なSSIDWi-Fi接続が切れる直前にlogcatで出力された内容

01-02 14:53:36.835  7059  7059 I wpa_supplicant: wlan0: CTRL-EVENT-ASSOC-REJECT bssid=**:**:**:**:**:** status_code=40

ここではログの表示内容に特に注目する。 先頭のwpa_supplicantはアプリケーションである。 名前通りにWPA等のWi-Fiの認証を処理するクライアントである。 これはLinuxで広く使われているから、調べれば詳細が得られる。

続いて、末尾のstatus_code=40に注目する。 この番号は「802.11 Association Status Codes」と呼ばれ、名前から分かる通りIEEEで標準化されているようだ。 だから実装に寄らず意味は共通である。 原典はIEEE文書であり、無料で参照できない可能性が高いから代わりのリンクを次に記載しておく。

802.11 Association Status Codes & Codes

40の意味は次のように定義されていた。

Invalid information element, i.e., an information element defined in this standard for which the content does not meet the specifications in Clause 7

つまり、wpa_supplicantはWi-Fiルーター/APの示す情報が規格違反だと思っているようだ。 恐らく、Androidバージョンが低いために同梱されているwpa_supplicantもバージョンが低く、WPA3への対応が不完全なのだろう。

不本意だが、このAndroid端末用に「WPA2-PersonalのSSIDを用意する」ことで解決した。

付録

ADBのlogcatでは非常に大量のログが出力されるために、Wi-Fiに関連するログだけを抽出することが難しい。 万能ではないが、手掛かりや糸口を掴むために有用かもしれないヒントを残しておく。

wpa_supplicantに注目する

上で説明した通り、wpa_supplicantはWi-Fiの認証を処理するプログラムである。 ほとんどの場合、SSIDへの接続に先立って認証が行われるから、とりあえずwpa_supplicantのログを監視しておけば「頻繁に切断・再接続が行われているか」を確認できる。 logcatサブコマンドは-sオプションでフィルタができるから、以下のようにして絞り込むと良い。

./adb logcat -s wpa_supplicant

その他のフィルタ方法や詳細は./adb logcat --helpや公式のドキュメントを参照せよ。

全て取得して要らないものだけ落とす

Wi-Fiは複数のモジュールやプログラムが協調して動作しているために、単純に特定の文字列を含むログだけ見ていると真の原因を見逃す危険性がある。 例えば、単純に「wifi」を含む行だけを抽出しても情報が得られない。 「wlan」や上記「wpa_supplicant」、「IpReachabilityMonitor」などいくつもの要因がある。 そこで、「とりあえず全てのログを取得し、明らかに無関係のログだけを除外する」調査方針が有効だ。 logcatの出力をパイプでつないで、フィルタコマンドで除外するやり方が良いだろう。

WindowsではLinuxの定番のフィルタコマンド群が使えないから、代わりにSelect-Stringを使うと良い。 今回の調査では以下のようなログを除外した。

# logcatで全てのログをそのままパイプに流し、Select-Stringで'MEMSIC_Calibrating', 'sunwave', 'HWCDisplay'が含まれないログだけを出力する
 ./adb logcat | Select-String -Pattern 'MEMSIC_Calibrating', 'sunwave', 'HWCDisplay' -NotMatch