CUBE SUGAR CONTAINER

技術系のこと書きます。

systemd で nftables の設定を永続化する

今回は nftables のスクリプトを systemd から読み込むことで設定を永続化する方法について。 結論から述べると systemctl cat nftables で読み込んでいるファイルの場所を確認したら、そこにルールを書けば良い。

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

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.2 LTS
Release:    24.04
Codename:   noble
$ uname -srm
Linux 6.8.0-59-generic x86_64
$ nft --version
nftables v1.0.9 (Old Doc Yak #3)

もくじ

下準備

まずは nftables をインストールする。 一般的な環境であれば最初から入っているはず。

$ sudo apt-get -y install nftables

UFW (Uncomplicated Firewall) は、nftables と同時に利用すると競合しやすい。 そのため、もし使っている場合には無効にする。

$ sudo systemctl stop ufw
$ sudo systemctl disable ufw

そして、systemd で nftables のサービスを動かす。

$ sudo systemctl start nftables
$ sudo systemctl enable nftables

設定ファイルの場所を確認する

まずは nftables のサービスが、どこの設定ファイルを読むのか確認する。 systemctl cat でユニットファイルの内容を見るのが手っ取り早い。

$ systemctl cat nftables | grep -i ^exec
ExecStart=/usr/sbin/nft -f /etc/nftables.conf
ExecReload=/usr/sbin/nft -f /etc/nftables.conf
ExecStop=/usr/sbin/nft flush ruleset

上記から /etc/nftables.conf を読んでいることが確認できる。

デフォルトの設定を確認する

先ほど確認した設定ファイルの内容を見てみよう。 すると input, forward, output hook に base チェインが設定されている。 単なる入れ物が用意されているだけで、すべての通信が accept される状態になっている。

$ cat /etc/nftables.conf 
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority filter;
    }
    chain forward {
        type filter hook forward priority filter;
    }
    chain output {
        type filter hook output priority filter;
    }
}

上記の設定ファイルの内容がシステムに反映されているか確認してみよう。

nft list ruleset コマンドを実行すると、先ほどの設定ファイルと同じ内容が確認できる。

$ sudo nft list ruleset
table inet filter {
    chain input {
        type filter hook input priority filter; policy accept;
    }

    chain forward {
        type filter hook forward priority filter; policy accept;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

これは、先ほど systemd の nftables サービスを開始したため設定が読み込まれている。

nftables の設定ファイルを編集してみる

続いては systemd のサービスが読んでいる nftables の設定ファイルを編集してみよう。

以下では /etc/nftables にディレクトリを作って、そこに nftables のスクリプトを用意している。 内容は基本的な設定を入れたファイアウォールになっている。

$ sudo mkdir -p /etc/nftables
$ cat << 'EOF' | sudo tee /etc/nftables/simple-firewall.nft
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
   chain input {
      type filter hook input priority 0; policy drop;
      # 関連・確立済みのコネクションは通す
      ct state established,related accept
      # 不正なコネクションは落とす
      ct state invalid drop
      # ループバックは通す
      iif lo accept
      # ICMPv4 の特定タイプは通す
      ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept
      # ICMPv6 の特定タイプは通す
      ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
      # SSH (TCP/22) はレートリミットつきで通す
      tcp dport ssh limit rate 10/minute accept
   }

   chain forward {
      type filter hook forward priority 0; policy drop;
   }

   chain output {
      type filter hook output priority 0; policy accept;
   }

}
EOF

上記の設定を /etc/nftables.conf から include する。

$ cat << 'EOF' | sudo tee /etc/nftables.conf >/dev/null
#!/usr/sbin/nft -f

flush ruleset

include "/etc/nftables/simple-firewall.nft"
EOF

この状態で systemd の nftables サービスをリロードする。

$ sudo systemctl reload nftables

すると、先ほどのファイルに書いた内容が動作に反映される。

$ sudo nft list ruleset
table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;
        ct state established,related accept
        ct state invalid drop
        iif "lo" accept
        ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept
        ip6 nexthdr ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
        tcp dport 22 limit rate 10/minute burst 5 packets accept
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

再起動して設定が永続化されていることを確認する

念の為、システムを再起動しても設定が反映され直すことを確認しておこう。

$ sudo shutdown -r now

再起動が終わったら、もう一度ログインして nftables の設定を確認する。

$ sudo nft list ruleset
table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;
        ct state established,related accept
        ct state invalid drop
        iif "lo" accept
        ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept
        ip6 nexthdr ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
        tcp dport 22 limit rate 10/minute burst 5 packets accept
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

ちゃんと、先ほどと同じ設定が読み込まれていることが確認できる。

めでたしめでたし。

nftables の処理をトレース機能 (meta nftrace) で追跡する

nftables は、Linux の Netfilter サブシステムをバックエンドに実装されたフレームワークのひとつ。 nftables を使うことで、パケットフィルタリングや NAT、パケット分類などを統一的に管理できる。 nftables は、xtables (iptables, ip6tables など) を置き換える後継として開発された。

今回は、その nftables が管理しているルールがどのように動いているかをトレース機能 (meta nftrace) を使って調べる方法について。 nftables のルールをデバッグする際、素朴なやり方ではログやカウンタを用いるやり方がある。 それに比べてトレース機能を使うと、より詳細は情報が得られる。

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

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.2 LTS
Release:    24.04
Codename:   noble
$ uname -srm
Linux 6.8.0-58-generic x86_64
$ nft --version
nftables v1.0.9 (Old Doc Yak #3)

もくじ

下準備

あらかじめ、必要なパッケージをインストールしておく。

$ sudo apt-get -y install nftables iproute2 iputils-ping

実験用の Network Namespace を用意する

ホストを直接使って nftables の実験をすると不都合が多い。 そこで、Network Namespace を使って隔離されたネットワークスタックを用意する。 今回は 2 つの Network Namespace を用意して、それぞれを veth でつなぐ。

まずは Network Namespace を用意する。

$ sudo ip netns add ns1
$ sudo ip netns add ns2

両者をつなぐための veth を作る。

$ sudo ip link add ns1-veth0 type veth peer name ns2-veth0

veth の両端を Network Namespace に所属させる。

$ sudo ip link set ns1-veth0 netns ns1
$ sudo ip link set ns2-veth0 netns ns2

veth デバイスの MAC アドレスをドキュメンテーションアドレスに変更しておく。

$ sudo ip netns exec ns1 ip link set dev ns1-veth0 address 00:00:5E:00:53:01
$ sudo ip netns exec ns2 ip link set dev ns2-veth0 address 00:00:5E:00:53:02

インターフェイスの状態を UP にする。

$ sudo ip netns exec ns1 ip link set ns1-veth0 up
$ sudo ip netns exec ns2 ip link set ns2-veth0 up

インターフェイスにドキュメンテーションアドレスの IP アドレスを付与する。

$ sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0
$ sudo ip netns exec ns2 ip address add 192.0.2.2/24 dev ns2-veth0

この状態で、一旦 ping による疎通があるかを確認しておく。

$ sudo ip netns exec ns1 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.034 ms
64 bytes from 192.0.2.2: icmp_seq=2 ttl=64 time=0.017 ms
64 bytes from 192.0.2.2: icmp_seq=3 ttl=64 time=0.030 ms

--- 192.0.2.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2074ms
rtt min/avg/max/mdev = 0.017/0.027/0.034/0.007 ms

以降は Network Namespace の ns1 に nftables の設定を投入して実験していく。

トレース対象のルールを追加する

まず、初期状態では nftables に何も設定が入っていない。 nftables に設定されているルールは nft list ruleset コマンドで確認できる。

$ sudo ip netns exec ns1 nft list ruleset

ここに nft -f で設定を投入する。 引数にハイフンを指定することで、標準入力から設定を読み込むことができる。

以下の設定では、inet アドレスファミリの filter テーブルの中に、input チェインがある。 input チェインは type filter hook input なので、Netfilter の input hook 経由でパケットが入ってくる。 そして、input チェインには ip protocol icmp icmp type echo-request というルールが含まれる。 これは ICMPv4 の Echo Request、つまりは Ping の往路と一致するルールになっている。

$ cat << 'EOF' | sudo ip netns exec ns1 nft -f -
table inet filter {
    chain input {
        type filter hook input priority 0; policy accept
        ip protocol icmp icmp type echo-request
    }
}
EOF

上記を実行すると、Network Namespace の ns1 に nftables の設定が入る。 なお、このルールは一致したとしても何もしない。 また、チェインのデフォルトのポリシーが accept なので、パケットはそのまま通過する。

$ sudo ip netns exec ns1 nft list ruleset
table inet filter {
    chain input {
        type filter hook input priority filter; policy accept;
        ip protocol icmp icmp type echo-request
    }
}

上記のルールによってパケットが処理される様子をトレース機能で追跡したい。

トレース用のルールを追加する

続いては、トレース機能を使うためのルールを追加する。 nftables のトレース機能 (meta nftrace) を使うには、パケットにトレースを有効にするメタ情報のフラグをつける必要がある。 トレース機能のメタ情報のフラグが有効になったパケットは、以降の処理でモニター用の機能を使って追跡できるようになる。 そこで、メタ情報のフラグを付与するためのルールが必要になる。

今回は、input hook よりも早く処理される prerouting hook に、トレース用のルールを追加しよう。 以下の設定では、inet アドレスファミリの filter テーブルの中に、prerouting チェインを作っている。 prerouting チェインは type filter hook prerouting なので、Netfilter の prerouting hook 経由でパケットが入ってくる。 そして、prerouting チェインには ip protocol icmp meta nftrace set 1 というルールが含まれる。 これは ICMPv4 のパケットにトレース機能のフラグを付与している。

$ cat << 'EOF' | sudo ip netns exec ns1 nft -f -
table inet filter {
    chain prerouting {
        type filter hook prerouting priority 0; policy accept
        ip protocol icmp meta nftrace set 1
    }
}
EOF

設定を投入すると、ルールセットは次のようになる。

$ sudo ip netns exec ns1 nft list ruleset
table inet filter {
    chain input {
        type filter hook input priority filter; policy accept;
        ip protocol icmp icmp type echo-request
    }

    chain prerouting {
        type filter hook prerouting priority filter; policy accept;
        ip protocol icmp meta nftrace set 1
    }
}

処理の流れをモニターする

この状態で nft monitor trace コマンドを実行しよう。 実行すると出力を待ち受けた状態になる。

$ sudo ip netns exec ns1 nft monitor trace

ここで、別のターミナルを開いて ns2 から ns1 に向けて Ping を打ってみよう。

$ sudo ip netns exec ns2 ping -c 1 192.0.2.1
PING 192.0.2.1 (192.0.2.1) 56(84) bytes of data.
64 bytes from 192.0.2.1: icmp_seq=1 ttl=64 time=0.051 ms

--- 192.0.2.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.051/0.051/0.051/0.000 ms

すると、先ほど実行した nft monitor trace コマンドに出力が得られる。

$ sudo ip netns exec ns1 nft monitor trace
trace id 2d55095a inet filter prerouting packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 23062 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 3186 icmp sequence 1 @th,64,96 0x455136800000000f21b0100 
trace id 2d55095a inet filter prerouting rule ip protocol icmp meta nftrace set 1 (verdict continue)
trace id 2d55095a inet filter prerouting policy accept 
trace id 2d55095a inet filter input packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 23062 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 3186 icmp sequence 1 @th,64,96 0x455136800000000f21b0100 
trace id 2d55095a inet filter input rule ip protocol icmp icmp type echo-request (verdict continue)
trace id 2d55095a inet filter input policy accept 

上記から、以下の部分で ICMP の Echo Request のパケットに prerouting のルールでメタ情報が付与され、そのまま通過してチェインのデフォルトのポリシーで accept された様子が確認できる。

trace id 2d55095a inet filter prerouting packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 23062 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 3186 icmp sequence 1 @th,64,96 0x455136800000000f21b0100 
trace id 2d55095a inet filter prerouting rule ip protocol icmp meta nftrace set 1 (verdict continue)
trace id 2d55095a inet filter prerouting policy accept 

同様に、以下の部分では input のルールに一致した後に、そのまま通過してチェインのデフォルトのポリシーで accept されたことが確認できる。

trace id 2d55095a inet filter input packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 23062 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 3186 icmp sequence 1 @th,64,96 0x455136800000000f21b0100 
trace id 2d55095a inet filter input rule ip protocol icmp icmp type echo-request (verdict continue)
trace id 2d55095a inet filter input policy accept 

このように、ルールでトレース機能を有効化して、それをモニターすることで nftables の処理の流れを把握できる。

まとめ

今回は nftables で処理の流れを追跡してルールのデバッグに活かすことのできるトレース機能 (meta nftrace) について扱った。

今回の例では、メタ情報を付与するための専用のチェインとルールを追加していた。 しかし、必要な区間でピンポイントにメタ情報を付与して、必要なくなったらメタ情報を取り除くといったことも考えられる。

また、nft monitor trace の出力は場合によっては大量になることから grep(1) などを用いて必要な内容だけに絞り込むのも良いようだ。

参考

wiki.nftables.org


Python: パッケージ・プロジェクトマネージャの uv を使ってみる

今回は Rust で書かれた Python のパッケージ・プロジェクトマネージャの uv を使ってみる。

これまで Python では複数のツールを組み合わせて開発のワークフローを構築するのが一般的だった。 そのような構成では、それぞれのツールは目的に特化しているので、単独ではシンプルに扱える。 一方で、目的ごとにツールをどのように組み合わせるかユーザ自身で考えなければいけない点は敷居が高かった。 そのような状況で、uv は以下のツール群を単独で置き換えることができるとしている。

  • pip
  • pip-tools
  • pipx
  • poetry
  • pyenv
  • twine
  • virtualenv

上記のツールを見ると、要するに Python の実行環境、仮想環境、パッケージ、プロジェクトの管理をひとつのツールで代替できそうなことがうかがえる。

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

$ sw_vers
ProductName:        macOS
ProductVersion:     15.4.1
BuildVersion:       24E263
$ uv -V
uv 0.6.14 (Homebrew 2025-04-09)

もくじ

下準備

uv をインストールする方法はいくつかある。 インストーラのシェルスクリプトを実行したり、pip でインストールしたり。 今回は macOS なので Homebrew を使うのが一番楽だろう。

$ brew install uv

インストールすると uv コマンドが使えるようになる。

$ uv -V
uv 0.6.14 (Homebrew 2025-04-09)

初期設定

使い始める前にやっておくと良さそうな設定について。

uv generate-shell-completion コマンドを使うと、シェルごとの補完 (Completion) 用の設定が得られる。

$ uv generate-shell-completion <shell>

たとえば zsh を使っているのであれば、設定ファイル (~/.zshrc など) に次のようなコードを追加しておけば良い。 uv コマンドが使える環境で補完用の設定が入るようになる。

if which uv >/dev/null 2>&1; then
  eval "$(uv generate-shell-completion zsh)"
fi

Scripts 機能

uv はいくつかの機能を提供している。 一番シンプルな機能として、まずは Scripts という機能を紹介する。 この機能を使うと、Python のスクリプトに実行環境や依存関係に関するメタデータを付与できる。 メタデータを付与したスクリプトは uv を使ってメタデータを解決した形で実行できる。

たとえば、次のようにサードパーティー製のパッケージである requests を使うスクリプトを用意する。

$ cat << 'EOF' > fetch.py
import requests


def main():
    response = requests.get("https://example.org")
    print(f"status code: {response.status_code}")


if __name__ == "__main__":
    main()
EOF

この状態でシステムの Python を使って実行してみる。 このとき、requests がインストールされていない環境であればエラーになる。

$ python3 fetch.py 
Traceback (most recent call last):
  File "/Users/amedama/Documents/temporary/uv/fetch.py", line 1, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

スクリプトに依存パッケージのメタデータを登録する

この状態で、uv add サブコマンドを使う。 このサブコマンドは uv が管理するプロジェクトやスクリプトに依存関係のパッケージを登録するのに使う。 ここでは、先ほど用意したスクリプトの依存パッケージに requests を登録する。 実行する際にはオプションとして --script をつけて、スクリプトと依存パッケージを続ける。

$ uv add --script fetch.py requests
Updated `fetch.py`

すると、スクリプトの先頭部分にコメントでメタデータが付与される。

$ head fetch.py                    
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "requests",
# ]
# ///
import requests


def main():

登録されたメタデータを解決してスクリプトを実行する

この状態で uv run サブコマンドを使ってスクリプトを実行する。 このとき、オプションとして --script をつける。

$ uv run --script fetch.py
Installed 5 packages in 5ms
status code: 200

すると、メタデータを読み取って依存パッケージをインストールした仮想環境上でスクリプトが実行される。

内部的にどんなことをやっているかは -v オプションをつけて実行すると分かりやすい。

$ uv run -v --script fetch.py
DEBUG uv 0.6.14 (Homebrew 2025-04-09)
DEBUG Reading inline script metadata from `fetch.py`
DEBUG Acquired lock for `/Users/amedama/Documents/temporary/uv/fetch.py`
DEBUG Using Python request Python >=3.12 from `requires-python` metadata
DEBUG Checking for Python environment at `/Users/amedama/.cache/uv/environments-v2/fetch-e04e193531c37c15`
DEBUG The script environment's Python version satisfies `>=3.12`
DEBUG Released lock at `/var/folders/p2/g5ntz9vn46bbdvrl0pfkfpnm0000gn/T/uv-e04e193531c37c15.lock`
DEBUG All requirements satisfied: certifi>=2017.4.17 | charset-normalizer>=2, <4 | idna>=2.5, <4 | requests | urllib3>=1.21.1, <3
DEBUG Using Python 3.12.10 interpreter at: /Users/amedama/.cache/uv/environments-v2/fetch-e04e193531c37c15/bin/python3
DEBUG Running `python fetch.py`
DEBUG Spawned child 75859 in process group 75858
status code: 200
DEBUG Command exited with code: 0

上記から、以下の仮想環境上で実行されていることが確認できる。

$ /Users/amedama/.cache/uv/environments-v2/fetch-e04e193531c37c15/bin/python3 -V
Python 3.12.10

スクリプトに登録されている依存パッケージを確認する

スクリプトに登録されている依存関係は uv tree サブコマンドで確認できる。

$ uv tree --script fetch.py 
Resolved 5 packages in 2ms
requests v2.32.3
├── certifi v2025.1.31
├── charset-normalizer v3.4.1
├── idna v3.10
└── urllib3 v2.4.0

スクリプトに登録されている依存パッケージを削除する

なお、依存パッケージを削除したいときは uv remove サブコマンドを使う。

$ uv remove --script fetch.py requests
Updated `fetch.py`

Python versions 機能

続いては Python versions という機能について。 これは pyenv をイメージすると分かりやすい。 要するに異なるバージョンや実装の Python をインストールする機能になる。

インストール済みの Python 実行環境を確認する

まず、現在インストールされている Python は uv python list サブコマンドで確認できる。 --only-installed オプションをつけることでインストール済みのバージョンだけに絞られる。 以下を見て分かる通り、uv 以外でインストールした Python も PATH が通っていれば検出されるようだ。

$ uv python list 
cpython-3.12.10-macos-aarch64-none    /opt/homebrew/bin/python3.12 -> ../Cellar/python@3.12/3.12.10/bin/python3.12
cpython-3.9.6-macos-aarch64-none      /usr/bin/python3

--only-installed をつけずに実行すると、主要なマイナーバージョンについて最新パッチバージョンの結果が得られる。 また、--all-versions をつけると過去のすべてのバージョンを含んだ結果が得られる。

$ uv python list --all-versions | head
cpython-3.14.0a6-macos-aarch64-none                 <download available>
cpython-3.14.0a6+freethreaded-macos-aarch64-none    <download available>
cpython-3.13.3-macos-aarch64-none                   <download available>
cpython-3.13.3+freethreaded-macos-aarch64-none      <download available>
cpython-3.13.2-macos-aarch64-none                   <download available>
cpython-3.13.2+freethreaded-macos-aarch64-none      <download available>
cpython-3.13.1-macos-aarch64-none                   <download available>
cpython-3.13.1+freethreaded-macos-aarch64-none      <download available>
cpython-3.13.0-macos-aarch64-none                   <download available>
cpython-3.13.0+freethreaded-macos-aarch64-none      <download available>

Python の実行環境をインストールする

特定のバージョンをインストールしたいときは uv python install サブコマンドを使う。 ためしに Python 3.13 をインストールしてみよう。

$ uv python install 3.13
Installed Python 3.13.3 in 3.34s
 + cpython-3.13.3-macos-aarch64-none

以下のように Python 3.13 がインストールされた。

$ uv python list | grep 3.13
cpython-3.13.3-macos-aarch64-none                   /Users/amedama/.local/share/uv/python/cpython-3.13.3-macos-aarch64-none/bin/python3.13
cpython-3.13.3+freethreaded-macos-aarch64-none      <download available>

CPython だけでなく PyPy など異なる実装もインストールできる。

$ uv python list --all-platforms | grep ^pypy | head -n 5
pypy-3.11.11-windows-x86_64-none                       <download available>
pypy-3.11.11-macos-x86_64-none                         <download available>
pypy-3.11.11-macos-aarch64-none                        <download available>
pypy-3.11.11-linux-x86_64-gnu                          <download available>
pypy-3.11.11-linux-x86-gnu                             <download available>

たとえば PyPy のバージョン 3.11 を入れたいときは次のようにする。

$ uv python install pypy@3.11
Installed Python 3.11.11 in 4.61s
 + pypy-3.11.11-macos-aarch64-none

Python の実行環境がインストールされるディレクトリを確認する

インストール先のディレクトリは uv python dir サブコマンドで確認できる。

$ uv python dir
/Users/amedama/.local/share/uv/python

使用する Python のバージョンを指定する

デフォルトで使用したい Python のバージョンを指定したいときは uv python pin サブコマンドを使う。

$ uv python pin 3.13
Pinned `.python-version` to `3.13`

とはいえ、これは単に .python-version というファイルをカレントワーキングディレクトリに作成するだけ。

$ cat .python-version    
3.13

uv は、ディレクトリのファイルを読んでデフォルトの Python を選択するようだ。 つまり、特定のディレクトリで使う Python を指定していることになる。

$ uv run python -V
Python 3.13.3
$ uv python pin 3.12
Updated `.python-version` from `3.13` -> `3.12`
$ uv run python -V  
Python 3.12.10

ディレクトリ単位ではなく、ユーザが使用するデフォルトを指定したいときは --global オプションをつける。 すると、~/.config/uv 以下にファイルが作られる。

$ uv python pin 3.12 --global
Pinned `/Users/amedama/.config/uv/.python-version` to `3.12`

上記はユーザのデフォルトなので、もしディレクトリに .python-version があるときはそちらが優先される。

$ uv python pin 3.13         
Updated `.python-version` from `3.12` -> `3.13`
$ uv run python -V  
Python 3.13.3

インストール済みの Python インタプリタへのパスを取得する

uv が認識している各バージョンの Python インタプリタへのパスは uv python find サブコマンドで得られる。

$ uv python find 3.13
/Users/amedama/.local/share/uv/python/cpython-3.13.3-macos-aarch64-none/bin/python3.13
$ uv python find 3.12
/opt/homebrew/opt/python@3.12/bin/python3.12

Python の実行環境をアンインストールする

Python の実行環境をアンインストールしたいときは uv python uninstall サブコマンドを使う。

$ uv python uninstall 3.13
Searching for Python versions matching: Python 3.13
Uninstalled Python 3.13.3 in 74ms
 - cpython-3.13.3-macos-aarch64-none
$ rm .python-version

Projects 機能

続いては Projects 機能について。 uv のユースケースとしては、この機能を使う場面が一番多そう。 この機能は poetry をイメージすると分かりやすい。 プロジェクトは、Python を使って開発する何らかのアプリケーションやパッケージになる。

プロジェクトを作成する

まずは uv init サブコマンドでプロジェクトを作成する。 以下では helloworld という名前でプロジェクトを作っている。 --vcs none をつけているのは、デフォルトではプロジェクトに空の Git リポジトリを作成するため。 今回はバージョン管理の部分が不要なので明示している。

$ uv init helloworld --vcs none
Initialized project `helloworld` at `/Users/amedama/Documents/temporary/uv/helloworld`

なお、上記ではコマンドの後ろにプロジェクト名を指定している。 この場合はプロジェクト名でサブディレクトリが作られる。 これ以外に、自分でディレクトリを作った上で、そこで uv init するやり方もある。

$ mkdir helloworld
$ cd helloworld
$ uv init  --vcs none

uv init を実行すると、次のようにプロジェクトに必要な最低限のファイルが用意される。

$ cd helloworld
$ ls -1a
.
..
.python-version
main.py
pyproject.toml
README.md

デフォルトで用意された Python のモジュール main.py は uv run コマンドで実行できる。

$ uv run main.py    
Using CPython 3.12.10 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12
Creating virtual environment at: .venv
Hello from helloworld!

実行するとプロジェクト用の仮想環境がディレクトリの配下に作られる。

$ ls -1a .venv
.
..
.gitignore
bin
CACHEDIR.TAG
lib
pyvenv.cfg

また、依存パッケージなどのバージョンやハッシュの情報を記載する uv.lock というファイルも用意される。

$ cat uv.lock        
version = 1
revision = 1
requires-python = ">=3.12"

[[package]]
name = "helloworld"
version = "0.1.0"
source = { virtual = "." }

.python-version は先述したとおり uv が読み取って実行する際のバージョンに使われる。

$ cat .python-version 
3.12

pyproject.toml は言わずとしれた Python でパッケージやプロジェクトを管理するメタデータを記載するファイルになっている。

$ cat pyproject.toml 
[project]
name = "helloworld"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

プロジェクトに依存パッケージを追加する

プロジェクトに依存パッケージを追加するときは uv add サブコマンドを使う。 この点は Scripts 機能を試したときと同じで、違いは --script <module> オプションの指定がないだけ。

$ uv add requests                  
Resolved 6 packages in 86ms
Prepared 5 packages in 162ms
Installed 5 packages in 5ms
 + certifi==2025.1.31
 + charset-normalizer==3.4.1
 + idna==3.10
 + requests==2.32.3
 + urllib3==2.4.0

パッケージの使用時ではなく開発のタイミングで必要になるものを追加するときは --dev をつける。

$ uv add --dev pytest    
Resolved 11 packages in 169ms
Prepared 4 packages in 117ms
Installed 4 packages in 5ms
 + iniconfig==2.1.0
 + packaging==24.2
 + pluggy==1.5.0
 + pytest==8.3.5

なお、--dev--group dev のエイリアスに過ぎない。 別の名称を使いたいときは --group オプションを使えば良い。 たとえば以下では testing というグループ名を使っている。

$ uv add --group testing pytest
Resolved 11 packages in 3ms
Audited 9 packages in 0.01ms

プロジェクトの依存パッケージを削除する

依存パッケージを削除したいときは uv remove を使う。 この点も Scripts 機能を試したときと変わらない。

$ uv remove --group testing pytest
Resolved 11 packages in 2ms
Audited 9 packages in 0.01ms

コマンドを実行すると pyproject.toml や uv.lock が更新される。 必要に応じて手動で編集すると良い。

$ cat pyproject.toml 
[project]
name = "helloworld"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "requests>=2.32.3",
]

[dependency-groups]
dev = [
    "pytest>=8.3.5",
]
testing = []

プロジェクトの環境を同期する

プロジェクトのリポジトリをチェックアウトした直後など、環境が整っていないときは uv sync サブコマンドを実行する。 試しに仮想環境を削除した上で実行してみよう。 すると、改めて仮想環境が作成されてパッケージがインストールされる。

$ rm -rf .venv 
$ uv sync                         
Using CPython 3.12.10 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12
Creating virtual environment at: .venv
Resolved 11 packages in 0.51ms
Installed 9 packages in 6ms
 + certifi==2025.1.31
 + charset-normalizer==3.4.1
 + idna==3.10
 + iniconfig==2.1.0
 + packaging==24.2
 + pluggy==1.5.0
 + pytest==8.3.5
 + requests==2.32.3
 + urllib3==2.4.0

プロジェクトの依存パッケージを確認する

プロジェクトが依存しているパッケージは uv tree サブコマンドで確認できる。 この点も Scripts 機能と変わらない。

$ uv tree                   
Resolved 11 packages in 0.56ms
helloworld v0.1.0
├── requests v2.32.3
│   ├── certifi v2025.1.31
│   ├── charset-normalizer v3.4.1
│   ├── idna v3.10
│   └── urllib3 v2.4.0
└── pytest v8.3.5 (group: dev)
    ├── iniconfig v2.1.0
    ├── packaging v24.2
    └── pluggy v1.5.0

コマンドの入るパッケージの扱いについて

なお、パッケージによっては仮想環境の bin ディレクトリにコマンドがインストールされることがある。 たとえば Ruff であれば ruff コマンドが入る。

$ uv add --dev ruff
Resolved 12 packages in 455ms
Prepared 1 package in 1.66s
Installed 1 package in 8ms
 + ruff==0.11.5

インストールされたコマンドを呼びたいときは uv run 経由で実行できる。

$ uv run -- ruff -V               
ruff 0.11.5

あるいは、単純に仮想環境をアクティベートすることで PATH を通してしまっても良い。

$ source .venv/bin/activate
(helloworld) $ ruff -V         
ruff 0.11.5

仮想環境から抜けるときは deactivate する。

(helloworld) $ deactivate

パッケージの配布物をビルドして公開する

プロジェクトの pyproject.toml を元にパッケージの配布物をビルドして公開できる。 配布物を公開する部分は twine をイメージすると分かりやすい。

まずは uv build サブコマンドを使って配布物をビルドする。

$ uv build

これで、ソース配布物 (sdist) や Wheel ファイルができる。

$ ls -1 dist
helloworld-0.1.0-py3-none-any.whl
helloworld-0.1.0.tar.gz

そして、uv publish すると PyPI に公開できる。 デフォルトではインタラクティブにアカウントの情報を入力する。

$ uv publish

とはいえ、ここらへんは手作業でやるよりも CI/CD の仕組みを作り込むのが一般的だろう。

Tools 機能

先ほどの Projects 機能では、プロジェクトに紐づいた依存パッケージの管理について学んだ。 とはいえ、プロジェクトに紐づかない普段遣いのツールをグローバルに入れたい場合もあるはず。 そのようなときは Tools 機能を用いる。

ツールをインストールする

たとえば black が使いたいなーと思ったときは uv tool install サブコマンドを使う。 すると、Tools 機能用の仮想環境にパッケージがインストールされる。

$ uv tool install black
Resolved 6 packages in 174ms
Prepared 5 packages in 282ms
Installed 6 packages in 6ms
 + black==25.1.0
 + click==8.1.8
 + mypy-extensions==1.0.0
 + packaging==24.2
 + pathspec==0.12.1
 + platformdirs==4.3.7
Installed 2 executables: black, blackd
warning: `/Users/amedama/.local/bin` is not on your PATH. To use installed tools, run `export PATH="/Users/amedama/.local/bin:$PATH"` or `uv tool update-shell`.

コマンドは $HOME/local/bin にデフォルトでインストールされる。 もし、ここに PATH が通っていない場合には上記のように警告が出る。

警告にあるとおり、手動でシェルの設定を投入するか、あるいは uv tool update-shell サブコマンドを使う。

$ uv tool update-shell
Created configuration file: /Users/amedama/.zshenv
Restart your shell to apply changes

uv tool update-shell サブコマンドを使うとシェルにあわせて設定ファイルを作ってくれる。 普段使いする環境なら、手動で設定を投入する方が挙動をコントロールできて良いかな。

$ cat ~/.zshenv 
# uv
export PATH="/Users/amedama/.local/bin:$PATH"

ツールを実行する

インストールしたツールは uv tool run サブコマンドで実行できる。

$ uv tool run black main.py 
All done! ✨ 🍰 ✨
1 file left unchanged.

あるいは uvx というエイリアスもあるようだ。

$ uvx black main.py    
All done! ✨ 🍰 ✨
1 file left unchanged.

もちろんコマンドに PATH が通っていれば、シェルでそのまま実行しても構わない。

$ which black 
/Users/amedama/.local/bin/black
$ black --version
black, 25.1.0 (compiled: yes)
Python (CPython) 3.12.10

インストール済みのツールを確認する

インストール済みのパッケージは uv tool list サブコマンドで確認できる。

$ uv tool list        
black v25.1.0
- black
- blackd

実行時の Python のバージョンを指定したいときは --python オプションを使うと良い。

$ uv tool run --python 3.11 black main.py

インストール済みのツールを更新する

インストール済みのパッケージのバージョンを上げるときは uv tool upgrade を使う。 --all オプションをつけるとすべてのパッケージを更新できる。

$ uv tool upgrade --all

ツールをアンインストールする

パッケージをアンインストールするときは uv tool uninstall サブコマンドを使う。

$ uv tool uninstall black
Uninstalled 2 executables: black, blackd

pip Interface 機能

uv には pip Interface という機能がある。 この機能を使うことで、既存の pip や venv / virtualenv を使った開発の中でも uv の恩恵を得ることができる。

仮想環境を作成する

まず、uv venv サブコマンドを使うと仮想環境を作成できる。 デフォルトではカレントワーキングディレクトリに .venv という名前で仮想環境ができる。

$ cd ../
$ uv venv                         
Using CPython 3.12.10 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate

もし名前を指定して作成したいときはサブコマンドに続けて仮想環境の名前を指定する。

$ uv venv myvenv
Using CPython 3.12.10 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12
Creating virtual environment at: myvenv
Activate with: source myvenv/bin/activate

作成した仮想環境は、一般的な venv / virtualenv を使った仮想環境と同様に source でアクティベートする。

$ source .venv/bin/activate

uv の pip Interface は、仮想環境を次の優先順位で探索する。

  • VIRTUAL_ENV 環境変数
    • uv / venv / virtualenv で作成した環境をアクティベートすると自動で設定される
  • CONDA_PREFIX 環境変数
    • Conda で作成した環境をアクティベートすると自動で設定される
  • カレントワーキングディレクトリの .venv ディレクトリ

今回は明示的にアクティベートしたので一番上のルールに該当する。

$ echo $VIRTUAL_ENV
/Users/amedama/Documents/temporary/uv/.venv

uv の pip Interface は、pip の操作方法の一部を移植している。 内部的に pip を利用しているわけではないらしい。

パッケージをインストールする

たとえば uv pip install を使って環境にパッケージをインストールできる。

$ uv pip install pylint
Resolved 7 packages in 209ms
Prepared 6 packages in 196ms
Installed 7 packages in 6ms
 + astroid==3.3.9
 + dill==0.4.0
 + isort==6.0.1
 + mccabe==0.7.0
 + platformdirs==4.3.7
 + pylint==3.3.6
 + tomlkit==0.13.2

インストール済みのパッケージを確認する

uv pip list サブコマンドでインストール済みのパッケージ一覧を確認できる。

$ uv pip list          
Package      Version
------------ -------
astroid      3.3.9
dill         0.4.0
isort        6.0.1
mccabe       0.7.0
platformdirs 4.3.7
pylint       3.3.6
tomlkit      0.13.2

パッケージの依存関係を確認する

それぞれのパッケージの依存関係は uv pip tree の方が見やすい。

$ uv pip tree       
pylint v3.3.6
├── astroid v3.3.9
├── dill v0.4.0
├── dill v0.4.0
├── isort v6.0.1
├── mccabe v0.7.0
├── platformdirs v4.3.7
└── tomlkit v0.13.2

パッケージのバージョンに矛盾がないか確認する

インストールしたパッケージが要求するバージョンに矛盾がないか uv pip check で確認できる。

$ uv pip check
Checked 7 packages in 0.39ms
All installed packages are compatible

インストール済みのパッケージのバージョンを確認する

インストールしたパッケージのバージョンは uv pip freeze で確認する。

$ uv pip freeze
astroid==3.3.9
dill==0.4.0
isort==6.0.1
mccabe==0.7.0
platformdirs==4.3.7
pylint==3.3.6
tomlkit==0.13.2

インストールしたパッケージの詳細を確認する

uv pip show サブコマンドでインストールしたパッケージの詳細を確認できる。

$ uv pip show pylint
Name: pylint
Version: 3.3.6
Location: /Users/amedama/Documents/temporary/uv/.venv/lib/python3.12/site-packages
Requires: astroid, dill, isort, mccabe, platformdirs, tomlkit
Required-by:

パッケージをアンインストールする

パッケージをアンインストールしたいときは uv pip uninstall を使う。

$ uv pip uninstall pylint
Uninstalled 1 package in 11ms
 - pylint==3.3.6

pyproject.toml でインストールされるパッケージの詳細を確認する

uv pip compile を使うと pyproject.toml から入るパッケージとバージョンを確認できる。 これは pip にはない機能かな。 似たようなことをするならまっさらな環境に pip install . した上で pip freeze するのかな。

$ cd helloworld
$ uv pip compile pyproject.toml 
Resolved 5 packages in 65ms
# This file was autogenerated by uv via the following command:
#    uv pip compile pyproject.toml
certifi==2025.1.31
    # via requests
charset-normalizer==3.4.1
    # via requests
idna==3.10
    # via requests
requests==2.32.3
    # via helloworld (pyproject.toml)
urllib3==2.4.0
    # via requests

pyproject.toml からパッケージをインストールする

uv pip sync を使うことで、pyproject.toml などから仮想環境にパッケージをインストールできる。 こちらも本来の pip にはない機能で、似たようなことがしたいときは pip install -U . とかするかな。

$ uv pip sync pyproject.toml 
Using Python 3.12.10 environment at: /Users/amedama/Documents/temporary/uv/.venv
Resolved 1 package in 1ms
Uninstalled 6 packages in 14ms
Installed 1 package in 2ms
 - astroid==3.3.9
 - dill==0.4.0
 - isort==6.0.1
 - mccabe==0.7.0
 - platformdirs==4.3.7
 + requests==2.32.3
 - tomlkit==0.13.2

その他

uv のキャッシュしているデータを削除したいときは uv cache cleanuv cache prune が使える。

$ uv cache clean            
Clearing cache at: /Users/amedama/.cache/uv
Removed 1527 files (56.1MiB)

キャッシュのあるディレクトリ自体は uv cache dir サブコマンドで得られる。

$ uv cache dir  
/Users/amedama/.cache/uv

uv をインストール用のスクリプトを使って入れた場合には uv self update サブコマンドで uv 自体を更新できる。 今回のように Homebrew などのパッケージマネージャ経由で入れたときはエラーになっておわり。

$ uv self update
error: uv was installed through an external package manager, and self-update is not available. Please use your package manager to update uv.

まとめ

今回は Python のパッケージおよびプロジェクトマネージャの uv を使ってみた。 様々なツールを uv だけで代替できるところは便利に感じられる。 一方で、動作する際に内部的な振る舞いが予想しにくく感じられる場面もあった。 今後は Projects 機能を中心に、適材適所で使っていきたい。

Ubuntu 24.04 LTS に TightVNC を入れて macOS から GUI で操作する

GNU/Linux ディストリビューションのシステムは、大抵の操作を CUI で完結できる。 とはいえ、たまに特定の操作を GUI のフロントエンドでやりたくなることもある。 そんなとき、わざわざ筐体にディスプレイなどをつないで操作するのは手間がかかる。 そもそも、操作したいマシンと物理的に離れていることも多い。

そのような場面で、マシンを GUI で操作する方法はいくつか考えられる。 最もシンプルなのは SSH で X11 Forwarding することだろう。 その次に、もうちょっとリッチなデスクトップ環境が使いたいときに検討する選択肢のひとつが VNC だと思う。

今回は、Ubuntu 24.04 LTS に TightVNC を入れて macOS から操作する方法について書く。 このとき、接続には SSH Port Forwarding を使うことで外部には VNC 用のポートを公開しないようにする。

VNC サーバを入れた Ubuntu の環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.2 LTS
Release:    24.04
Codename:   noble
$ uname -srm
Linux 6.8.0-57-generic x86_64
$ tightvncserver --help 2>&1 | head -n 1
TightVNC Server version 1.3.10

VNC クライアントに使った macOS の環境は次のとおり。

$ sw_vers            
ProductName:        macOS
ProductVersion:     15.4
BuildVersion:       24E248
$ ssh -V      
OpenSSH_9.9p1, LibreSSL 3.3.6

もくじ

下準備

あらかじめ TightVNC と、デスクトップ環境として xfce4 をインストールしておく。

$ sudo apt-get install tightvncserver xfce4

VNC 接続用のパスワードを設定する

まずは tightvncpasswd コマンドを実行して VNC 用のパスワードを設定する。 このパスワードは、VNC クライアントからサーバに接続する際の認証に使われる。 使用するパスワードの長さは 6 ~ 8 文字になっている必要があるらしい。 ただし、今回の構成では SSH PortForwarding することが前提なので、その部分でセキュリティを担保する。 ここで設定するパスワードに関してはおまけのような感じ。

$ tightvncpasswd 
Using password file /home/amedama/.vnc/passwd
VNC directory /home/amedama/.vnc does not exist, creating.
Password: 
Verify:   
Would you like to enter a view-only password (y/n)? n

TightVNC のサーバを起動する

次に、tightvncserver コマンドを使って TightVNC のサーバを起動する。 このとき、必ず -localhost のオプションを指定する。 これでホストの外部に VNC 用のポートが公開されない。

$ tightvncserver -localhost -nolisten tcp -nevershared :1

その他のオプションについては、-nolisten tcp では Xserver へのリモート接続を抑制している。 そして -nevershared では、複数のクライアントから接続があった際にそれぞれで状態を共有しないようにする。 末尾の :1 では、仮想ディスプレイ番号 1 番で待ち受けることを指定している。 VNC サーバが使用するポート番号は、5900 に仮想ディスプレイの番号を足したものになる。

サーバを起動すると、ループバックアドレスで TCP の 5901 番を待ち受けていることが確認できる。

$ ss -tlnp | grep tight
LISTEN 0      5          127.0.0.1:5901       0.0.0.0:*    users:(("Xtightvnc",pid=4107,fd=1))

macOS から TightVNC のサーバに接続する

次に macOS から TightVNC のサーバへ接続しにいく。 まずは macOS から TightVNC をインストールしたマシンへ SSH でログインする。 このとき、リモートの 5901 番ポートをローカルの 5901 番ポートに対応する形で Port Forwarding する。

$ ssh -L 5901:localhost:5901 <remote>

上記で <remote> の部分は TightVNC をインストールしたマシンに付与した IP アドレスやドメイン名に読みかえる。

SSH で Port Forwarding できたら、macOS の「Finder > 移動 > サーバへ接続」を開く。 「サーバへ接続」の画面が開いたらサーバとして「vnc://localhost:5901」を入力して「接続」ボタンを押下する。

Finder > 移動 > サーバへ接続

すると、パスワードの入力を求められるので最初の工程で設定した VNC 用のパスワードを入力する。

VNC 用のパスワードを入力する

すると、次のようにそっけない画面が表示される。

初期状態のデスクトップ

ひとまず、これで VNC で接続できることが確認できた。

接続時に xfce4 のデスクトップ環境を起動する

次に、VNC で接続した際に xfce4 のデスクトップ環境を起動するように設定する。 まずは、デフォルトの設定用スクリプトをバックアップしておく。

$ cp ~/.vnc/xstartup{,.bak}

もし、何かあったときはバックアップから元に戻せば良い。

その上で、xfce4 の環境を起動する内容を設定用スクリプトに書き込む。

$ cat << 'EOF' > ~/.vnc/xstartup
#!/bin/sh

xrdb "$HOME/.Xresources"
startxfce4 &
EOF

設定したら、今起動している TightVNC のサーバを停止する。 これには tightvncserver -kill :<仮想ディスプレイ番号> を実行する。

$ tightvncserver -kill :1

停止したら、改めて起動する。 これで先ほどの設定用スクリプトが読み込まれるようになる。

$ tightvncserver -localhost -nolisten tcp -nevershared :1

あとは、先ほどと同じ手順でもう一度 macOS から VNC サーバに接続する。 すると、今度は次のようにちゃんとしたデスクトップ環境が表示される。

xfce4 のデスクトップ環境が表示される

あとは、上記の画面を使って必要な操作を進める。

そして、使い終わったら VNC サーバを停止する。

$ tightvncserver -kill :1

いじょう。

macOS に LHa for UNIX をソースコードからインストールする

Unix で LZH 形式の圧縮ファイルを展開できるソフトウェアはいくつかある。 たとえば lhasa 1 や unar 2 が挙げられる。

一方で、Unix で LZH 形式の圧縮ファイルを作成できるソフトウェアは限られる。 その限られた選択肢のひとつである LHa for UNIX が先日 Homebrew の core から削除された。

理由は LHa for UNIX が独自のライセンスを採用していたため。 これにより Homebrew が定めるライセンス要件を満たさないと判断された。

github.com

上記に対応するプルリクエストは 2025-03-24 にマージされている。

github.com

今となっては LZH 形式のアーカイブを扱う人はさほど多くないとは思われる。 しかも、展開ではなく作成とくればなおさら。 とはいえ、選択肢が限られた状況で困る人もいるかもしれないのでここにメモしておく。

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

$ sw_vers
ProductName:        macOS
ProductVersion:     15.4
BuildVersion:       24E248
$ uname -srm    
Darwin 24.4.0 arm64

もくじ

事前準備

もし Xcode の Command Line Tools が入っていないときはインストールする。

$ xcode-select --install

同様に、Homebrew が入っていないときはインストールする。

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

ただし、Homebrew のインストールで叩くコマンドは稀に変わることがあるので公式サイトを確認した方が良い。

brew.sh

LHa for UNIX のビルドに必要なパッケージをインストールする。

$ brew install autoconf automake

LHa for UNIX のリポジトリをクローンする。

$ git clone https://github.com/jca02266/lha.git
$ cd lha

上記のリポジトリはもはやほとんど更新されることが無いため、基本的に HEAD を使って問題ないと思われる。

ビルドする

ビルドに必要な一連のコマンドを叩く。 ここらへんは README を読めば書いてある。

$ aclocal
$ autoheader
$ automake -a
$ autoconf
$ sh ./configure
$ make

これで src ディレクトリ以下に lha という名前のバイナリができる。

$ file src/lha
src/lha: Mach-O 64-bit executable arm64
$ ./src/lha --help | head -n 1
LHa for UNIX version 1.14i-ac20220213 (aarch64-apple-darwin24.3.0)

念の為にテストも実行してすべてパスすることを確認しておこう。

$ make check

インストールする

あとはバイナリをパスが通っている場所に置くだけ。 手動で mv などしても良いだろうけど古式ゆかしく make install もできる。

$ sudo make install

これでデフォルトなら /usr/local/bin に入るはず。

$ lha --help | head -n 1    
LHa for UNIX version 1.14i-ac20220213 (aarch64-apple-darwin24.3.0)

LZH の圧縮ファイルを作成する

動作を確認する。 まずは適当なテキストファイルを用意する。 念の為にハッシュも見ておく。

$ echo "Hello, World" > greet.txt
$ md5sum greet.txt 
9af2f8218b150c351ad802c6f3d66abe  greet.txt

上記を LZH ファイルに圧縮する。

$ lha a greet.lzh greet.txt 
greet.txt   - Frozen(100%)o

ちゃんとできた。

$ file greet.lzh 
greet.lzh:   LHarc 1.x/ARX archive data  [lh0], 'U' OS

LZH の圧縮ファイルを展開する

次に、元のテキストファイルを削除する。

$ rm greet.txt

そして圧縮ファイルの内容を展開する。 元のファイルを削除しておかないとファイルが重複してしまう。

$ lha e greet.lzh           
greet.txt   - Melted   :  o

展開されたファイルの内容が元のファイルと同一であることを確認する。

$ cat greet.txt            
Hello, World
$ md5sum greet.txt 
9af2f8218b150c351ad802c6f3d66abe  greet.txt

別のアーカイバでも展開する

念の為に別のアーカイバも使って展開しておく。 Homebrew で unar をインストールする。

$ brew install unar

先ほど展開したテキストファイルを削除しておく。

$ rm greet.txt

unar を使って LZH ファイルを展開する。

$ unar greet.lzh
greet.lzh: LZH
  greet.txt  (13 B)... OK.
Successfully extracted to "./greet.txt".

もう一度、展開されたファイルの内容が元のファイルと同一であることを確認する。

$ cat greet.txt
Hello, World
$ md5sum greet.txt 
9af2f8218b150c351ad802c6f3d66abe  greet.txt

以上、問題なく動作しちえることが確認できた。

ASRock DeskMini X600 で省スペースな Linux マシンを組んでみた

仮想マシンをいくつか作成して、簡単な検証をする時に使う Linux マシンが欲しくなった。 結果として、ASRock DeskMini X600 というベアボーンキットを使って小型のマシンを組んだ。 今回は、その内容についてメモしておく。

ASRock DeskMini X600

もくじ

概要

前述した通り、一度に数台程度の仮想マシンを動かすためのマシンが欲しくなった。 要件として考えた項目は次の通り。

  • なるべく部屋のスペースを占有しない
  • コストパフォーマンスで見劣りしない
  • さほど消費電力が大きくない
    • 動作時に数十ワット程度を想定する
  • なるべく動作音が小さい
  • 何らかの GNU/Linux ディストリビューションが動作する
    • Ubuntu や Proxmox VE など

選択肢について

省スペースでコスパが良いとなると、中国メーカー製のミニ PC や、Lenovo の ThinkCentre シリーズが思い浮かぶ。 実際、以前に Lenovo の ThinkCentre シリーズは M75q Tiny Gen2 を購入したことがある。 とはいえ、購入したことのある機材をただ増やすだけというのもあんまり面白くない。

blog.amedama.jp

そんな折、ASRock DeskMini という省スペースなコンピュータを組むためのベアボーンキットのシリーズがあることを知った。 ベアボーンキットなので、ケース、マザーボード、電源、そして CPU クーラーがあらかじめセットになっている。 ちなみに、筐体のサイズや電源の容量が一回り大きな DeskMeet というシリーズもあるようだ。

ASRock DeskMini は CPU のソケットや使用するチップセットごとに、いくつかのモデルが用意されている。 その中でも DeskMini X600 は TDP が 65W までの Socket AM5 の AMD CPU に対応したモデルになる。 今だと発売時よりも値段が下がっていて 3 万円を切る価格で購入できる。

構成を検討する

実際にコンピュータとして動作させるには、ベアボーンキットの他に CPU、メモリ、ストレージが必要になる。 その上でパーツの構成を検討してみると、10 万円前後の予算で組めそうなことが分かった。 なお、価格は購入先とタイミングによって変動する。

パーツ 製品 価格 (購入時点)
ベアボーンキット ASRock DeskMini X600 ¥25,980
CPU AMD Ryzen 5 8600G ¥33,136
メモリ Crucial DDR5-5600 32GBx2 (SO-DIMM) ¥23,480
SSD Crucial T500 1TB ¥11,600
CPUクーラー Scythe SHURIKEN 3 ¥3,980

これなら、他の選択肢を選んだときに想定されるスペックと価格に対してさほど見劣りしない。 そして、ベアボーンキットならお手軽に自作の楽しさも味わえる点も魅力に映った。

なお、予算を下げるとしたら次のような選択肢がありそう。

  • CPU を下位モデルの Ryzen 5 8500G にする (¥-10k)
  • メモリの容量を半分の 32GB に減らす (¥-10k)
  • ベアボーンキットに付属する CPU クーラーを使用する (¥-4k)

下位モデルの CPU の Ryzen 5 8500G は、8600G と同じ 6 コア 12 スレッドながら 4 コア分が高効率コアの Zen5c になっている。 ただ、TDP はどちらも 65W と変わらないようなので上位モデルを選んだ。 ちなみに、Passmark のベンチマーク (マルチコア) で比較すると 8600G が 25338 で 8500G が 21666 となっていた。

なお、CPU としては 8000 番台と同様に Socket AM5 を採用している 7000 番台や 9000 番台も使用できる。 DeskMini X600 は dGPU の追加が難しいことから iGPU が強力な APU の 8000 番台を組み合わせることが多いようだ。 今回に関してはグラフィックス性能は不要なので [79]000 番台の CPU も選定の候補には挙がった。 ただ、選定のタイミングではコストパフォーマンスに優れたモデルが見当たらなかったため選択しなかった。 また、[79]000 番台を選ぶ場合の注意点としては、以下が挙げられる。

  • TDP が 65W のモデルを選択すること
  • 8000 番台に比べてアイドル電力が 10W ほど大きいこと

CPU クーラーに関しては、ベアボーンキットに付属する CPU クーラーは動作音が大きいというレビューが多いようだった。 そのため、評判の良さそうな Scythe SHURIKEN 3 にしている。 なお、下調べでは Noctua NH-L9a-AM5 を使っている人も多いようだった。 ただし、メーカーの互換性ページを見ると 8600G はベースクロックでの動作時に冷やしきれない (cannot handle) とある。

ncc.noctua.at

公式に互換性が確認されているのは 8500G までのようだった。 発熱は消費電力と相関するはずなので、TDP は同じでも高効率コアを採用している点で両者は特性が異なるのかもしれない。

購入後について

実際に、試算に基づいてひととおりのパーツを購入した。

購入したパーツたち

上記を組み立てていく。

まずは DeskMini X600 の背面のネジを外すと、背面のプレートごとマザーボードが引き出せる。

背面のネジを外してマザーボードを引き出す

CPU を取り付ける

CPU を取り付ける。 取り付け方は一般的な Socket AM5 と変わらない。

  1. レバーを押し下げてクリップから外す
  2. ロードプレートを持ち上げて開く
  3. CPU とソケットの角にある三角形のマークを合わせて CPU をソケットに載せる
  4. ロードプレートを閉じる
  5. レバーを押し下げてクリップに固定する
  6. ソケットを保護しているキャップを取り除く

CPU を取り付ける

なお、この後に塗布する CPU グリスのはみ出しを防ぎたいときや冷却性能を上げたいときはサーマルペーストガードやガードプレートと呼ばれる部品を使うと良い。

CPU クーラーを取り付ける

CPU を取り付けたら、次に CPU クーラーを取り付ける。 DeskMini X600 に付属している CPU クーラーは、マウントホールに取り付けられた既存のブラケットに引っかけるタイプらしい。 一方で、今回選定した Scythe SHURIKEN 3 はソケットごとに専用のマウンティングプレートを使用する。 そのため、マザーボードに取り付けられているブラケットは取り外す。

マウントホールのブラケットを取り外す

取り外したら、CPU クーラーを取り付ける。

  1. 付属する Socket AM5 用のマウンティングプレートを CPU クーラーに装着する
  2. CPU クーラーに付属するサーマルペーストを CPU の表面に塗布する
  3. 付属するネジで CPU クーラーをマザーボードに固定する

CPU クーラーを取り付ける

メモリを取り付ける

次にメモリを取り付ける。 特に気にするようなことはない。

  1. スロットの固定用のクリップを左右に広げる
  2. 切り欠きの位置を揃えて、メモリのモジュールを上からスロットへ押し込む
  3. 固定用のクリップが自動的に閉じる

メモリを取り付ける

SSD を取り付ける

次に SSD を取り付ける。 今回はヒートシンクが付属しないモデルを購入した。 そのため、別途購入したヒートシンクを取り付けた。

サーマルパッドとヒートシンクで SSD をサンドイッチにする。

SSD にヒートシンクを取り付ける

あとは M.2 スロットに差し込んで、DeskMini X600 に付属するネジでマザーボードに固定するだけ。

なお、M.2 スロットは 2 つある。 また、2.5 インチの SATA HDD/SSD も 2 つまで搭載できる。

起動する

これで必要なパーツはすべて組み込めた。 AC アダプタの電源をつないで起動する。 UEFI BIOS が立ち上がって、組み込んだパーツを認識していることを確かめる。

UEFI BIOS が起動してパーツを認識していることを確かめる

メモリをテストする

メモリの初期不良がないか memtest86+ などを使って確認する。 やり方については以下のエントリに書いた。

blog.amedama.jp

OS をインストールする

ひとまず、Ubuntu 24.04 LTS をインストールして使うことにした。 USB メモリに Ubuntu 24.04 LTS のインストーライメージを書き込む。

書き込んだ USB メモリを筐体の USB ポートに差し込んで電源を入れたらインストーラが起動する。 あとはウィザードに従ってインストールするだけ。

まとめ

今回は ASRock DeskMini X600 というベアボーンキットを使って、小型の Linux マシンを組んだことについて書いた。 今のところ、特に問題なく使えている。

後日、筐体の側面にあるベンチレーションホールには以下のマグネット式のダストフィルターを貼り付けた。 140mm ファン用のフィルターでサイズがちょうど良い。 給排気の効率は少し落ちるかもしれないけど掃除の手間を優先した。

また、普段は電源を落として、使うときだけ WoL などで電源を入れるスタイルにしている。 詳しくは以下に書いた。

blog.amedama.jp

いじょう。

Ubuntu 24.04 LTS のマシンを Wake-on-LAN (WoL) でリモートから起動する

昨今の電気代などを考えると、コンピュータは使うとき以外はシャットダウンしておきたい。 アイドル時の電力が数十ワットでも、長く付けっぱなしだと年間で数千円にはなる。 ただ、出先で使いたくなったときなど物理的に電源を入れることが難しいシチュエーションもある。 もちろんサーバ向けの筐体であれば IPMI などの遠隔管理用のインターフェイスがあらかじめ用意されているけど、コンシューマ向けではそれも難しい。 後付けで遠隔管理用のカードを買い足すのにもお金がかかる。 そういったシチュエーションでは手っ取り早く Wake-on-LAN (以下、WoL) の利用が考えられる。 WoL を使うと、同一のネットワークにつなげることさえできれば遠隔からマシンの電源を入れることができる。

WoL はマジックパケットと呼ばれるデータに NIC (Network Interface Card) が反応してマシンの電源を入れてくれる仕組みのこと。 昨今は多くの NIC が WoL に対応している。 なお、前述した通り WoL を利用するには同一ネットワーク上にマジックパケットを送ることのできるマシンが必要になる。 それにはなるべく低消費電力な筐体で VPN や SSH サーバを建てたり、あるいは TailScale などの外部サービスを利用することが考えられる 1

使った環境は次のとおり。 WoL で起動するマシンが Ubuntu 24.04 LTS になる。 使った筐体は ASRock DeskMini X600 のオンボード LAN で、チップは Realtek 社製の RTL8125BG とのこと。

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=24.04
DISTRIB_CODENAME=noble
DISTRIB_DESCRIPTION="Ubuntu 24.04.2 LTS"
$ uname -srm
Linux 6.8.0-55-generic x86_64

そして WoL のマジックパケットを送るのには macOS のマシンを使った。

$ sw_vers         
ProductName:        macOS
ProductVersion:     15.3.2
BuildVersion:       24D81
$ wakeonlan --version        
wakeonlan 0.42

もくじ

下準備

WoL で起動されるマシンの側では、まず UEFI BIOS の設定で WoL を有効にする。 UEFI BIOS での設定方法は、マザーボードを製造しているメーカーやバージョンによって異なる。 参考までに、今回使用した ASRock のマザーボードであれば Advanced Mode に切り替えた上で Advanced タブから PCIE Devices Power On の項目を Enabled にすれば良い。

次に、Ubuntu 上では iproute2 と ethtool をインストールしておく。

$ sudo apt-get install iproute2 ethtool

そして、WoL を使う NIC の MAC アドレスを確認する。 MAC アドレスの確認には ip link show コマンドを使う。

$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 9c:6b:00:XX:XX:XX brd ff:ff:ff:ff:ff:ff

今回であれば上記 enp3s0 インターフェイスの MAC アドレス (9c:6b:00 から始まっている) を見る。 なお、上記で MAC アドレスの下位 24bit はマスクしている。

そして、マジックパケットを送って起動させる側の macOS では Homebrew で wakeonlan をインストールする。 Homebrew が入っていなければインストールする。

$ brew install wakeonlan

一度だけ WoL を有効にする

定常的に使うことは無いだろうけど、まずは動作確認のために ethtool を使ってシャットダウン後に一度だけ WoL を有効にしてみよう。

現状で WoL が有効かは ethtool にネットワークインターフェースの名前を指定することで確認できる。 以下のように Wake-on の値が d であれば WoL が無効になっている。

$ sudo ethtool enp3s0 | grep -i wake-on
    Supports Wake-on: pumbg
    Wake-on: d

一度だけ WoL を有効にするには、次のように -s オプション付きでネットワークインターフェースに wol g を指定する。 値に g を指定することで NIC がマジックパケットに反応するようになる。

$ sudo ethtool -s enp3s0 wol g

もう一度 ethtool を使って Wake-on の値が g になっていることを確認しよう。

$ sudo ethtool enp3s0 | grep -i wake-on
    Supports Wake-on: pumbg
    Wake-on: g

確認したらマシンの電源をシャットダウンする。

$ sudo shutdown -h now

シャットダウンできたら、マジックパケットを送る側のマシンに操作を移す。 wakeonlan コマンドに、先ほど確認した MAC アドレスを指定して実行しよう。

$ wakeonlan 9c:6b:00:XX:XX:XX
Sending magic packet to 255.255.255.255:9 with payload 9c:6b:00:XX:XX:XX
Hardware addresses: <total=1, valid=1, invalid=0>
Magic packets: <sent=1>

上手くいけばマジックパケットに反応してマシンが起動してくるはず。 ただし、先ほど ethtool を使って設定するやり方では永続化されないため、起動後は Wake-on の値が d に戻っている。

$ sudo ethtool enp3s0 | grep -i wake-on
    Supports Wake-on: pumbg
    Wake-on: d

Netplan で設定を永続化する

動作の確認ができたので、次は設定を永続化する。 Ubuntu 22.04 LTS 以降の Ubuntu はネットワークの管理にデフォルトで Netplan を使う。 そして、WoL の設定も Netplan で永続化できる。 Netplan で WoL を有効にするには、ネットワークインターフェースに wakeonlan: true という設定を入れれば良い。

今回使っているマシンでは /etc/netplan/01-fixed-ip-addr.yaml の設定ファイルでインターフェイスの設定をしている。 設定ファイルの名前は環境によって異なるので適宜読み替えてほしい。

初期の状態では、以下のように enp3s0 に DHCPv4 / DHCPv6 の設定を入れつつ静的に IP アドレスを振っている。

$ sudo cat /etc/netplan/01-fixed-ip-addr.yaml 
network:
    ethernets:
      enp3s0:
        dhcp4: true
        addresses:
          - 172.16.XXX.XXX/16
        dhcp4-overrides:
          use-routes: true
          use-dns: true
        dhcp6: true
    version: 2

上記に追加で wakeonlan: true の設定を入れる。

$ sudo cat /etc/netplan/01-fixed-ip-addr.yaml 
network:
    ethernets:
      enp3s0:
        dhcp4: true
        addresses:
          - 172.16.XXX.XXX/16
        dhcp4-overrides:
          use-routes: true
          use-dns: true
        dhcp6: true
        wakeonlan: true
    version: 2

この状態でマシンを再起動してみよう。

$ sudo shutdown -r now

すると、再起動した後でもネットワークインターフェイスに WoL が有効になっている。

$ sudo ethtool enp3s0 | grep -i wake-on
    Supports Wake-on: pumbg
    Wake-on: g

もちろんシャットダウンした上で WoL を使うこともできる。

$ sudo shutdown -h now

マジックパケットを送ってマシンが起動してくることを確認しよう。

$ wakeonlan 9c:6b:00:XX:XX:XX
Sending magic packet to 255.255.255.255:9 with payload 9c:6b:00:XX:XX:XX
Hardware addresses: <total=1, valid=1, invalid=0>
Magic packets: <sent=1>

ばっちりだ。

$ sudo ethtool enp3s0 | grep -i wake-on
    Supports Wake-on: pumbg
    Wake-on: g

めでたしめでたし。


  1. もし自身で適切に設定するのに自信がないときは TailScale などの実績が豊富な外部サービスを利用する方がセキュリティ的にはおすすめできる