CUBE SUGAR CONTAINER

技術系のこと書きます。

Mac: LM Studio に Hugging Face Hub からダウンロードしたモデルを CLI で読み込む

LM Studio は、ローカル LLM を動かすためのソフトウェアのひとつ。

lmstudio.ai

基本的に LM Studio では、モデルをダウンロードする際には Discover タブから GUI で検索してダウンロードできる。 しかし、Hugging Face Hub 1 などから手動でダウンロードしたモデルをインポートする方法も用意されている。 今回はそのやり方について書く。

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

$ sw_vers
ProductName:        macOS
ProductVersion:     15.5
BuildVersion:       24F74
$ uname -srm
Darwin 24.5.0 arm64
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M2 Pro
$ huggingface-cli version
huggingface_hub version: 0.33.0
$ lms version  
   __   __  ___  ______          ___        _______   ____
  / /  /  |/  / / __/ /___ _____/ (_)__    / ___/ /  /  _/
 / /__/ /|_/ / _\ \/ __/ // / _  / / _ \  / /__/ /___/ /  
/____/_/  /_/ /___/\__/\_,_/\_,_/_/\___/  \___/____/___/  

lms - LM Studio CLI - v0.0.41
GitHub: https://github.com/lmstudio-ai/lmstudio-cli

もくじ

下準備

まずは Homebrew から LM Studio をインストールする。

$ brew install --cask lm-studio

インストールすると $HOME/.lmstudio 以下にディレクトリができる。 この中の bin ディレクトリには LM Studio を CLI で操作するためのバイナリが入っている。 そこで、ここにパスを通す。

$ export PATH="$PATH:$HOME/.lmstudio/bin"

必要に応じてシェルの設定ファイルなどに永続化すると良い。

パスを通すと lms コマンドが使えるようになる。

$ lms version  
   __   __  ___  ______          ___        _______   ____
  / /  /  |/  / / __/ /___ _____/ (_)__    / ___/ /  /  _/
 / /__/ /|_/ / _\ \/ __/ // / _  / / _ \  / /__/ /___/ /  
/____/_/  /_/ /___/\__/\_,_/\_,_/_/\___/  \___/____/___/  

lms - LM Studio CLI - v0.0.41
GitHub: https://github.com/lmstudio-ai/lmstudio-cli

続いて、Hugging Face Hub を操作するためのパッケージをPyPI からダウンロードする。

$ pip install "huggingface_hub[cli]"

これで huggingface-cli コマンドが使えるようになる。

$ huggingface-cli version
huggingface_hub version: 0.33.0

モデルをダウンロードする

次に Hugging Face Hub からモデルをダウンロードする。 LM Studio はデフォルトの実行ランタイムとして llama.cpp を使用する。 そのため、GGUF のモデルをダウンロードする。

$ huggingface-cli download "lmstudio-community/gemma-3-1B-it-qat-GGUF" --local-dir .

モデルをインポートする

ダウンロードしたファイルを lms import コマンドでインポートする。 このときインポートのやり方を対話的に確認される。

$ lms import gemma-3-1B-it-QAT-Q4_0.gguf

無事にインポートできると LM Studio の画面でモデルが確認できるようになるはず。

LM Studio でインポートしたモデルが確認できる

モデルの動作を確認する

サーバを起動するとモデルが WebAPI 経由で使えるようになる。

試しにモデル一覧を curl(1) で確認してみよう。

$ curl http://localhost:1234/v1/models
{
  "data": [
    {
      "id": "gemma-3-1b-it-qat",
      "object": "model",
      "owned_by": "organization_owner"
    },
    {
      "id": "text-embedding-nomic-embed-text-v1.5",
      "object": "model",
      "owned_by": "organization_owner"
    }
  ],
  "object": "list"
}

インポートした gemma-3-1b-it-qat が確認できる。

同様に completions の API も使ってみよう。

$ curl -X POST -s http://localhost:1234/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
  "model": "gemma-3-1b-it-qat",
  "messages": [
    { "role": "user", "content": "こんにちは!" }
  ]
}'       
{
  "id": "chatcmpl-kfep0etrq6dz9fbcth25uh",
  "object": "chat.completion",
  "created": 1749874667,
  "model": "gemma-3-1b-it-qat",
  "choices": [
    {
      "index": 0,
      "logprobs": null,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "こんにちは!何かお手伝いできることはありますか? 😊 どんなことでもお気軽にご質問ください。"
      }
    }
  ],
  "usage": {
    "prompt_tokens": 11,
    "completion_tokens": 22,
    "total_tokens": 33
  },
  "stats": {},
  "system_fingerprint": "gemma-3-1b-it-qat"
}

問題ないようだ。


Mac: Ollama でローカル LLM を動かす

Ollama 1 はローカル LLM を動かすためのソフトウェアのひとつ。 一般的なラップトップやデスクトップマシンで動かすようなユースケースが主に想定されているように思う。 LLM の機能を不特定多数に提供するというよりは、マシンを操作しているユーザ自身が使用するイメージだろう。 似たようなユースケースで用いられるソフトウェアには、他にも LM Studio 2 などがある。 今回は、そんな Ollama を Mac から使う方法についてメモしておく。

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

$ sw_vers
ProductName:        macOS
ProductVersion:     15.5
BuildVersion:       24F74
$ uname -srm                
Darwin 24.5.0 arm64
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M2 Pro
$ ollama --version
ollama version is 0.9.0

もくじ

下準備

まず、Ollama は Homebrew を使ってインストールできる。

$ brew install --formula ollama

サーバを起動する

Ollama を利用するには、何をするにもまずはサーバのインスタンスを立ち上げる必要がある。

そのためには、Homebrew でインストールした場合には Ollama をサービスとして起動すれば良い。

$ brew services start ollama

あるいは、フォアグラウンドで実行したいときは単に ollama serve コマンドを叩いても良い。

$ ollama serve

設定を変更する

Ollama は、OLLAMA_ から始まる名前のシェル変数・環境変数を使って動作を変更できる。

たとえば、Homebrew のサービスからサーバを起動する場合には launchctl setenv で設定を変更する。 以下ではグローバルなコンテキスト長を 32768 トークンに変更している。

$ launchctl setenv OLLAMA_CONTEXT_LENGTH 32768

あるいは ollama serve コマンドで起動しているときは、単純にセッション変数として指定すれば良い。

$ OLLAMA_CONTEXT_LENGTH=32768 ollama serve

モデルをダウンロードする

続いてモデルをダウンロードするには ollama pull コマンドを使う。

以下では例として Gemma3 の 1B モデルを指定している。

$ ollama pull gemma3:1b
pulling manifest 
pulling 7cd4618c1faf: 100% ▕██████████████████▏ 815 MB                         
pulling e0a42594d802: 100% ▕██████████████████▏  358 B                         
pulling dd084c7d92a3: 100% ▕██████████████████▏ 8.4 KB                         
pulling 3116c5225075: 100% ▕██████████████████▏   77 B                         
pulling 120007c81bf8: 100% ▕██████████████████▏  492 B                         
verifying sha256 digest 
writing manifest 
success

Ollama が公式が提供しているモデルについては以下の Web ページで検索できる。

ollama.com

ダウンロードしたモデルは ollama ls コマンドで確認できる。

$ ollama ls           
NAME         ID              SIZE      MODIFIED    
gemma3:1b    8648f39daa8f    815 MB    6 hours ago

モデルの情報を得る

モデルの詳しい情報は ollama show コマンドで確認できる。

$ ollama show gemma3:1b
  Model
    architecture        gemma3     
    parameters          999.89M    
    context length      32768      
    embedding length    1152       
    quantization        Q4_K_M     

  Capabilities
    completion    

  Parameters
    stop           "<end_of_turn>"    
    temperature    1                  
    top_k          64                 
    top_p          0.95               

  License
    Gemma Terms of Use                  
    Last modified: February 21, 2024    
    ...                                 

ターミナルでモデルと対話する

ollama run コマンドを使うと、ターミナルを使ってモデルと対話できる。

$ ollama run gemma3:1b
>>> こんにちは!
こんにちは!何かお手伝いできることはありますか?😊 

どんなことでも構いません。例えば:

*   質問に答える
*   文章を作成する
*   アイデアを出す
*   情報検索をする

など、お気軽にお申し付けください。

なお、ollama ps コマンドを使うと、現在どのモデルがメモリ上で動作しているか確認できる。

$ ollama ps                     
NAME         ID              SIZE      PROCESSOR    UNTIL              
gemma3:1b    8648f39daa8f    2.2 GB    100% GPU     4 minutes from now

OpenAI-like な WebAPI を利用する

Ollama は OpenAI-like な Web API を提供している。 この Web API を通してモデルの機能を利用できる。

たとえば completions の API を curl(1) で叩いてみよう。 API はループバックアドレスの 11434 ポートで提供されている。

$ curl -s http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
  "model": "gemma3:1b",
  "messages": [
    { "role": "user", "content": "ご機嫌いかがですか?" }
  ]
}' | jq .

すると JSON で結果が得られる。

$ curl -s http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
  "model": "gemma3:1b",
  "messages": [
    { "role": "user", "content": "ご機嫌いかがですか?" }
  ]
}' | jq .
{
  "id": "chatcmpl-578",
  "object": "chat.completion",
  "created": 1749812263,
  "model": "gemma3:1b",
  "system_fingerprint": "fp_ollama",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "元気です、ありがとう! 😊 あなたはいかがですか? \n\n何かお手伝いできることはありますか?\n"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 16,
    "completion_tokens": 24,
    "total_tokens": 40
  }
}

すべての API のドキュメントは以下の Web ページで確認できる。

github.com

そして、API を通して様々なツールと連携できる。 連携できるツールは以下のページにまとめられている。

github.com

必要に応じて、先ほど叩いた API のエンドポイントをツールに設定することで連携が可能になる。

不要になったモデルを削除する

LLM のファイルはサイズが大きいので、不要になったときは消したくなることもある。

そんなときは ollama rm コマンドを使って削除できる。

$ ollama rm gemma3:1b 
deleted 'gemma3:1b'

いじょう。


Linux でスワップファイルを使ってスワップ領域のサイズを柔軟に変更する

一昔前だと、スワップ領域といえば専用のパーティションを用意して作るものというイメージがあった。 しかし、どうやら最近はファイルシステム上に作成したファイルを使ったスワップファイルの利用も盛んなようだ。 スワップファイルには、サイズを柔軟に変更できるメリットがある。 今回はスワップファイルを使ってスワップ領域のサイズを変更する方法について見ていく。

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

$ 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
$ mkswap --version
mkswap from util-linux 2.39.3
$ swapon --version
swapon from util-linux 2.39.3
$ fallocate --version
fallocate from util-linux 2.39.3

もくじ

スワップ領域の状況を確認する

スワップ領域の状況は procfs から確認できる。 具体的には /proc/swaps から使用しているスワップパーティションやスワップファイルの情報が得られる。

$ cat /proc/swaps 
Filename                Type        Size        Used    Priority
/swap.img                               file        3043324        0  -2

今回使用した環境では /swap.img というスワップファイルがスワップ領域に使われている。 サイズは 3GB ほどのようだ。

スワップファイルを使ってスワップ領域を増やす

現状を確認したところで、試しにスワップ領域を 4GB に増やしてみよう。

まずは fallocate(1) や dd(1) を使って特定のサイズを持ったファイルを作る。

$ sudo fallocate -l 4G /swapfile
$ ls -alF /swapfile 
-rw-r--r-- 1 root root 4294967296 May 13 10:46 /swapfile

上記では /swapfile というファイルパスに 4GB のサイズでファイルを作っている。

スーパーユーザ以外に読み書きできないようにパーミッションを変更する。

$ sudo chmod 600 /swapfile

mkswap(1) でスワップ領域として使用できるように初期化する。

$ sudo mkswap /swapfile
Setting up swapspace version 1, size = 4 GiB (4294963200 bytes)
no label, UUID=b724dc3f-02ec-4b2d-b137-8072132179e7

新しく作ったスワップファイルを swapon(1) でスワップ領域として有効化する。

$ sudo swapon /swapfile

古いスワップファイルは swapoff(1) でスワップ領域として無効化しておく。

$ sudo swapoff /swap.img

これでスワップ領域が 4GB になる。

$ cat /proc/swaps 
Filename                Type        Size        Used        Priority
/swapfile                               file        4194300        0      -2

スワップ領域の設定を fstab(5) で永続化する

先ほどのやり方では変更したスワップ領域の設定が永続化されない。 そのためマシンを再起動すると状況が元に戻ってしまう。

試しに再起動してみよう。

$ sudo shutdown -r now

もう一度ログインして確認すると /swap.img が使われている。

$ cat /proc/swaps 
Filename                Type        Size        Used        Priority
/swap.img                               file        3043324        0      -2

これは fstab(5) に設定を書き込んでいなかったために生じる。

$ grep "/swap.img" /etc/fstab 
/swap.img   none    swap    sw  0  0

そこで fstab(5) を書き換えてスワップファイルとして /swapfile が使われるようにしてみよう。

$ sudo sed -i.bak s:swap.img:swapfile: /etc/fstab

次のように /swapfile を使う形になる。

$ grep "/swapfile" /etc/fstab
/swapfile   none    swap    sw  0  0

以前のファイルは /etc/fstab.bak に残る。 何かあったときはこちらから元に戻そう。

$ grep "/swap.img" /etc/fstab.bak 
/swap.img   none    swap    sw  0  0

この状態でマシンを再起動する。

$ sudo shutdown -r now

もう一度ログインして確認すると、ちゃんとスワップ領域に /swapfile が使われている。

$ cat /proc/swaps 
Filename                Type        Size        Used        Priority
/swapfile                               file        4194300        0      -2

ばっちり。

まとめ

今回はスワップファイルを使ってスワップ領域のサイズを柔軟に変更する方法について確認した。 スワップファイルであれば、たとえば後から増設した高速な SSD を使ってスワップ領域を構築するといったこともやりやすい。


textlint を使って日本語の文章を校正する

textlint 1 は自然言語向けの Linter のひとつ。 対象とする文章を静的解析して、特定のルールに抵触していないか確認できる。 今回は macOS で textlint を使い始めるまでについてメモしておく。

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

$ sw_vers
ProductName:        macOS
ProductVersion:     15.4.1
BuildVersion:       24E263
$ uname -srm
Darwin 24.4.0 arm64
$ node --version  
v23.11.0
$ npm --version 
10.9.2
$ npx textlint --version    
v14.7.1

もくじ

下準備

textlint は npm で配布されている。 そこで、まずは Homebrew で Node.js をインストールする。

$ brew install node

textlint をインストールする

チェックしたい文章のある場所で npm を使って textlint をインストールする。 このとき --save-dev オプションをつけると package.json ファイルが作られる。

$ npm install --save-dev textlint

package.json には依存関係が書かれている。

$ cat package.json 
{
  "devDependencies": {
    "textlint": "^14.7.1"
  }
}

また、node_modules というディレクトリに textlint と依存パッケージがインストールされる。

$ ls -1 node_modules | head
@azu
@isaacs
@keyv
@pkgjs
@textlint
@types
ajv
ansi-regex
ansi-styles
argparse

これで textlint の本体がインストールできた。

校正用のプリセットルールをインストールする

次にチェックする具体的な内容の書かれたプリセットルールをインストールする。

ここでは例として日本語の技術文章向けのプリセットの textlint-rule-preset-ja-technical-writing を入れる。 プリセットも npm でインストールできる。

$ npm install --save-dev textlint-rule-preset-ja-technical-writing

その他にも textlint-ja というコミュニティのリポジトリを見ると色々なプリセットがある。

github.com

textlint の設定ファイルを用意する

次に、先ほどインストールしたプリセットを使う textlint の設定ファイルを用意する。 設定ファイルの名前は .textlintrc で、フォーマットは JSON になっている。

cat << 'EOF' > .textlintrc
{
    "rules": {
        "preset-ja-technical-writing": true
    }
}
EOF

文章をチェックする

サンプルとなる文章を用意する。

$ cat << 'EOF' > helloworld.md
吾輩は猫である。名前はまだ無い。
EOF

npx コマンドを使って textlint を呼び出して上記のファイルをチェックする。 すると ja-technical-writing/no-mix-dearu-desumasu というルールに抵触する箇所が見つかる。

$ npx textlint helloworld.md                                      

/Users/amedama/Documents/temporary/helloworld.md
  1:5  error  本文: "ですます"調 でなければなりません
=> "ですます"調 であるべき箇所に、次の "である"調 の箇所があります: "である。"
Total:
である  : 1
ですます: 0
  ja-technical-writing/no-mix-dearu-desumasu

✖ 1 problem (1 error, 0 warnings)

なお、npx コマンドを使わないパターンとして、コマンドに PATH を通してしまうやり方もある。 インストール先の bin ディレクトリは $(npm root) 以下の .bin になる。 つまり、以下のようにすれば良い。

$ PATH=$(npm root)/.bin:$PATH textlint helloworld.md

/Users/amedama/Documents/temporary/helloworld.md
  1:5  error  本文: "ですます"調 でなければなりません
=> "ですます"調 であるべき箇所に、次の "である"調 の箇所があります: "である。"
Total:
である  : 1
ですます: 0
  ja-technical-writing/no-mix-dearu-desumasu

✖ 1 problem (1 error, 0 warnings)

いじょう。


Homebrew のパッケージの情報を調べる

今回は Homebrew のパッケージについて諸々を調べる方法について。 毎回調べている気がするのでメモしておく。

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

$ sw_vers
ProductName:        macOS
ProductVersion:     15.4.1
BuildVersion:       24E263
$ uname -srm  
Darwin 24.4.0 arm64
$ brew --version                            
Homebrew 4.5.2

もくじ

下準備

あらかじめ Homebrew をインストールしておく。 やり方は公式の Web サイト 1 を参照のこと。

基本的な情報を確認する

まずはパッケージの基本的な情報について知りたいときは brew info <package> を使う。 バージョンやライセンス、Webサイト、インストール用の Ruby スクリプトの場所など色々と確認できる。

$ brew info jq
==> jq: stable 1.7.1 (bottled), HEAD
Lightweight and flexible command-line JSON processor
https://jqlang.github.io/jq/
Not installed
Bottle Size: 525.0KB
Installed Size: 1.4MB
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/j/jq.rb
License: MIT
==> Dependencies
Required: oniguruma ✘
==> Options
--HEAD
    Install HEAD version
==> Analytics
install: 34,169 (30 days), 103,697 (90 days), 519,279 (365 days)
install-on-request: 33,728 (30 days), 102,033 (90 days), 511,906 (365 days)
build-error: 0 (30 days)

インストール先のディレクトリを確認する

インストールされるディレクトリを確認するには brew --cellar <package> を使う。

$ brew --cellar jq
/opt/homebrew/Cellar/jq

このコマンドはインストールした後にパッケージに含まれるファイルを使って作業するときにも使うことがある。

パッケージに含まれるファイルを確認する

続いてはインストールした後にパッケージに含まれるファイルを確認する方法について。 まずは調査したいパッケージはインストールしておく。

$ brew install jq

そして brew list -v <package を実行する。

$ brew list -v jq
/opt/homebrew/Cellar/jq/1.7.1/INSTALL_RECEIPT.json
/opt/homebrew/Cellar/jq/1.7.1/bin/jq
/opt/homebrew/Cellar/jq/1.7.1/.brew/jq.rb
/opt/homebrew/Cellar/jq/1.7.1/ChangeLog
/opt/homebrew/Cellar/jq/1.7.1/AUTHORS
/opt/homebrew/Cellar/jq/1.7.1/NEWS.md
/opt/homebrew/Cellar/jq/1.7.1/include/jv.h
/opt/homebrew/Cellar/jq/1.7.1/include/jq.h
/opt/homebrew/Cellar/jq/1.7.1/sbom.spdx.json
/opt/homebrew/Cellar/jq/1.7.1/README.md
/opt/homebrew/Cellar/jq/1.7.1/COPYING
/opt/homebrew/Cellar/jq/1.7.1/lib/libjq.a
/opt/homebrew/Cellar/jq/1.7.1/lib/pkgconfig/libjq.pc
/opt/homebrew/Cellar/jq/1.7.1/lib/libjq.dylib
/opt/homebrew/Cellar/jq/1.7.1/lib/libjq.1.dylib
/opt/homebrew/Cellar/jq/1.7.1/share/man/man1/jq.1
/opt/homebrew/Cellar/jq/1.7.1/share/doc/jq/AUTHORS
/opt/homebrew/Cellar/jq/1.7.1/share/doc/jq/NEWS.md
/opt/homebrew/Cellar/jq/1.7.1/share/doc/jq/README.md
/opt/homebrew/Cellar/jq/1.7.1/share/doc/jq/COPYING

インストールに使われたスクリプトの内容を確認する

インストールしたパッケージは brew cat <package> でスクリプトの内容を確認できる。 もちろん、先ほど brew info にあったファイルを確認しても良い。

$ brew cat jq | head      
class Jq < Formula
  desc "Lightweight and flexible command-line JSON processor"
  homepage "https://jqlang.github.io/jq/"
  url "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-1.7.1.tar.gz"
  sha256 "478c9ca129fd2e3443fe27314b455e211e0d8c60bc8ff7df703873deeee580c2"
  license "MIT"

  livecheck do
    url :stable
    regex(/^(?:jq[._-])?v?(\d+(?:\.\d+)+)$/i)

いじょう。


nftablesは同じフックで優先度が後ろのルールがあるとaccept済みのパケットが再び評価される

nftables の公式 Wiki を眺めていたところ、気になる記述があった。 どうやら、nftables は同じフックポイントで、優先度が異なるチェーンがあるときに注意を要する振る舞いを示すようだ。

nftables の公式 Wiki の記述 1 を以下に引用する。

NOTE: If a packet is accepted and there is another chain, bearing the same hook type and with a later priority, then the packet will subsequently traverse this other chain. Hence, an accept verdict - be it by way of a rule or the default chain policy - isn't necessarily final. However, the same is not true of packets that are subjected to a drop verdict. Instead, drops take immediate effect, with no further rules or chains being evaluated.

以下に拙訳する。

注意: もしパケットが accept されても、他に同じフックタイプでより後ろの優先度のチェーンがあると、パケットはその別のチェーンを通過します。したがって、accept 判定はルールによるものであっても、デフォルトのチェーンポリシーであっても、それは必ずしも最終的なものではありません。ただし、drop 判定のパケットは同じことが当てはまりません。代わりに drop は即座に影響し、さらなるルールやチェインでは評価されません。

上記を見ると、一旦 accept されたパケットが異なるチェーンで再び評価されるらしい。 ただし、drop されたパケットについては再び評価されることはないようだ。 今回は、この振る舞いについて実際に動かして検証してみる。

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

$ 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 の設定を投入して実験していく。

同じフックで優先度が異なるルール (accept -> drop) を用意する

以下のコマンドでは Network Namespace の ns1 に nftables の設定を入れている。 まず、prerouting チェーンでは、prerouting フックでデバッグ用に ICMPv4 のパケットにトレース用のフラグを付与している。 これにより nft monitor trace でパケットを追跡できる。 input_pri0 チェーンでは、input フックの priority 0 で ICMPv4 の Echo-Request を accept している。 そして input_pri1 チェーンでは、同じ input フックの priority 1 で ICMPv4 の Echo-Request を drop している。 優先度では input_pri0 の方が input_pri1 よりも前になる。 つまり、ドキュメントの記述通りであれば input_pri0 で accept されたパケットは input_pri1 で再び評価されて drop されるはず。

$ cat << 'EOF' | sudo ip netns exec ns1 nft -f -
#!/usr/sbin/nft -f

flush ruleset

table inet filter {

    chain prerouting {
        type filter hook prerouting priority 0; policy accept
        # すべての ICMPv4 にトレース機能を有効にするメタ情報を付与する
        ip protocol icmp meta nftrace set 1
    }

   chain input_pri0 {
       # hook が input で priority 0   
       type filter hook input priority 0; policy accept;
       # ICMP Echo-Request を通す
       ip protocol icmp icmp type echo-request accept
   }

   chain input_pri1 {
       # hook が input で priority 1
       type filter hook input priority 1; policy accept;
       # ICMP Echo-Request を落とす
       ip protocol icmp icmp type echo-request drop
   }
}
EOF

設定が入ったことを確認する。

$ sudo ip netns exec ns1 nft -y list ruleset
table inet filter {
        chain prerouting {
                type filter hook prerouting priority 0; policy accept;
                ip protocol icmp meta nftrace set 1
        }

        chain input_pri0 {
                type filter hook input priority 0; policy accept;
                ip protocol icmp icmp type echo-request accept
        }

        chain input_pri1 {
                type filter hook input priority 1; policy accept;
                ip protocol icmp icmp type echo-request drop
        }
}

次に、パケットを追跡するために 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.

--- 192.0.2.1 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

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

$ sudo ip netns exec ns1 nft monitor trace
trace id 7a7d7959 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 398 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 2566 icmp sequence 1 @th,64,96 0xd893146800000000c2f80200
trace id 7a7d7959 inet filter prerouting rule ip protocol icmp meta nftrace set 1 (verdict continue)
trace id 7a7d7959 inet filter prerouting policy accept
trace id 7a7d7959 inet filter input_pri0 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 398 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 2566 icmp sequence 1 @th,64,96 0xd893146800000000c2f80200
trace id 7a7d7959 inet filter input_pri0 rule ip protocol icmp icmp type echo-request accept (verdict accept)
trace id 7a7d7959 inet filter input_pri1 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 398 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 2566 icmp sequence 1 @th,64,96 0xd893146800000000c2f80200
trace id 7a7d7959 inet filter input_pri1 rule ip protocol icmp icmp type echo-request drop (verdict drop)

上記で、以下はトレース機能のメタ情報を付与している様子 (meta nftrace set 1) を表している。

trace id 7a7d7959 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 398 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 2566 icmp sequence 1 @th,64,96 0xd893146800000000c2f80200
trace id 7a7d7959 inet filter prerouting rule ip protocol icmp meta nftrace set 1 (verdict continue)
trace id 7a7d7959 inet filter prerouting policy accept

そして、以下で input_pri0 でパケットが accept されている。

trace id 7a7d7959 inet filter input_pri0 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 398 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 2566 icmp sequence 1 @th,64,96 0xd893146800000000c2f80200
trace id 7a7d7959 inet filter input_pri0 rule ip protocol icmp icmp type echo-request accept (verdict accept)

しかし、同じ input フックで、より優先度が後ろの input_pri1 があるためパケットが再び評価される。 以下では input_pri1 で ICMPv4 の Echo Request が drop されている。

trace id 7a7d7959 inet filter input_pri1 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 398 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 2566 icmp sequence 1 @th,64,96 0xd893146800000000c2f80200
trace id 7a7d7959 inet filter input_pri1 rule ip protocol icmp icmp type echo-request drop (verdict drop)

ドキュメントにある通りの振る舞いを示すことが上記から確認できた。

同じフックで優先度が異なるルール (drop -> accept) を用意する

念の為、逆のパターンも確認しておこう。 同じフックで、より優先度が前のチェーンで drop されると、後ろのチェーンでは評価されないはず。

以下のように、先ほどと accept と drop するチェーンを入れ替えた設定を投入する。 今回は input_pri0 で drop して、input_pri1 で accept している。

$ cat << 'EOF' | sudo ip netns exec ns1 nft -f -
#!/usr/sbin/nft -f

flush ruleset

table inet filter {

    chain prerouting {
        type filter hook prerouting priority 0; policy accept
        # すべての ICMPv4 にトレース機能を有効にするメタ情報を付与する
        ip protocol icmp meta nftrace set 1
    }

   chain input_pri0 {
       # hook が input で priority 0   
       type filter hook input priority 0; policy accept;
       # ICMP Echo-Request を落とす
       ip protocol icmp icmp type echo-request drop
   }

   chain input_pri1 {
       # hook が input で priority 1
       type filter hook input priority 1; policy accept;
       # ICMP Echo-Request を通す
       ip protocol icmp icmp type echo-request accept
   }
}
EOF

投入した設定を確認する。

$ sudo ip netns exec ns1 nft -y list ruleset
table inet filter {
        chain prerouting {
                type filter hook prerouting priority 0; policy accept;
                ip protocol icmp meta nftrace set 1
        }

        chain input_pri0 {
                type filter hook input priority 0; policy accept;
                ip protocol icmp icmp type echo-request drop
        }

        chain input_pri1 {
                type filter hook input priority 1; policy accept;
                ip protocol icmp icmp type echo-request accept
        }
}

再び nft monitor torace コマンドを実行しておく。

$ 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.

--- 192.0.2.1 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

nft monitor trace に次のような出力が得られる。

$ sudo ip netns exec ns1 nft monitor trace
trace id ea548659 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 52494 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 10208 icmp sequence 1 @th,64,96 0x9fb71968000000002fb80000
trace id ea548659 inet filter prerouting rule ip protocol icmp meta nftrace set 1 (verdict continue)
trace id ea548659 inet filter prerouting policy accept
trace id ea548659 inet filter input_pri0 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 52494 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 10208 icmp sequence 1 @th,64,96 0x9fb71968000000002fb80000
trace id ea548659 inet filter input_pri0 rule ip protocol icmp icmp type echo-request drop (verdict drop)

以下では先ほどと同じようにトレース機能のメタ情報をパケットに付与している。

trace id ea548659 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 52494 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 10208 icmp sequence 1 @th,64,96 0x9fb71968000000002fb80000
trace id ea548659 inet filter prerouting rule ip protocol icmp meta nftrace set 1 (verdict continue)
trace id ea548659 inet filter prerouting policy accept

次に以下では input_pri0 でパケットが drop されている。

trace id ea548659 inet filter input_pri0 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 52494 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 10208 icmp sequence 1 @th,64,96 0x9fb71968000000002fb80000
trace id ea548659 inet filter input_pri0 rule ip protocol icmp icmp type echo-request drop (verdict drop)

そして、以降はトレース情報の出力がない。 したがって、drop された後は別のチェーンで処理されていない。 ドキュメント通りの振る舞いが確認できた。

いじょう。


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;
    }
}

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

めでたしめでたし。