0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LM Studioが0.0.0.0で待ち受けていた|ローカルLLM点検4項目

0
Posted at

LM Studioが0.0.0.0で待ち受けていた|ローカルLLM点検4項目

  • 自分の Mac で動かしている LM Studio の API サーバーが、LAN 全体に無認証で開いていた話と、その点検手順がわかる
  • コピペで動く公開範囲の点検スクリプト(読み取りのみ・設定を変更しない)が手に入る
  • --bind の指定先を変えたときにどこから届いてどこから届かなくなるかを実測した表が見られる
  • 対象読者: LM Studio / Ollama などをローカルで常駐させている人、Mac を自宅の LLM サーバーにしている人

ローカル LLM は「自分の手元で動くから安全」という感覚になりがちです。モデルの重みもプロンプトも外に出ない、というのは本当です。
ただ、それは サーバーが本当に自分の手元だけを向いているとき の話でした。

この記事は、自分の Mac mini を点検したら LM Studio の OpenAI 互換 API が *:1234 で待ち受けていて、同じ LAN から無認証で叩けた、という実測の記録です。攻撃手法ではなく、自分の資産の点検と塞ぎ方だけを扱います。

要点(TL;DR)

  • LM Studio の CLI (lms server start) の既定は 127.0.0.1 だが、GUI の「ローカルネットワークで配信」トグルを一度でも入れると設定ファイルに "networkInterface": "0.0.0.0" が永続化され、以後の自動起動でもそれが使われる。CLI の既定値を見て安心すると食い違う。
  • その状態の API は無認証。GET /v1/models がそのままモデル一覧を返す。API キーの概念が既定で存在しない。
  • さらに justInTimeModelLoading: true だと、リクエストを投げるだけでモデルのロードを発火できる構造になる(= メモリと時間を消費させられる)。
  • 点検は 4 項目で済む。①lsof でワイルドカード待ち受けを探す ②自分の LAN IP 経由で curl して応答を見る ③ファイアウォールの状態を見る ④LM Studio のサーバー設定を読む。下にスクリプトを置いた。
  • 塞ぎ方は lms server start --bind 127.0.0.1。ただし bind 先を Tailscale の tailnet IP など特定の IP にすると 127.0.0.1 からも届かなくなる(実測済み。後述)。

動作環境 / 前提

項目 値
ハード Mac mini (hw.model = Mac18,5) / Apple M6 / 統合メモリ 32GB
OS macOS 27.0.1 (build 26A434)
LM Studio デスクトップアプリ常駐 + lms CLI (CLI commit dcefd5a)
同居しているもの Docker Desktop、Tailscale
確認日 2026-11-02

以下のコマンド結果はすべてこの環境での実測です。IP は自宅の private アドレスと Tailscale の tailnet IP なので、記事中では一部を伏せています。

1. 何が起きていたか

まず待ち受けアドレスを見ます。lsof の NAME 列(末尾の (LISTEN) の直前)が待ち受け先です。

lsof -nP -iTCP:1234 -sTCP:LISTEN

実際の出力(抜粋):

COMMAND     PID   USER             FD   TYPE  DEVICE  NODE NAME
LM\x20Stu 32818  numazakiryotaro  78u  IPv4  0x64ae  TCP  *:1234 (LISTEN)

*:1234 は「全インターフェースで待ち受け」の意味です。127.0.0.1:1234 なら自分のマシン限定ですが、* や 0.0.0.0 だと LAN 側の NIC でも受けます。

本当に外から届くのかは、自分の LAN IP を指定して自分で叩けばわかります(他人の機器は叩かない)。

LANIP=$(ipconfig getifaddr en0)
curl -s -m 5 -o /dev/null -w "http=%{http_code}\n" "http://$LANIP:1234/v1/models"

結果:

http=200

200 が返りました。つまり同じ LAN にいる機器から、認証なしでモデル一覧が取れる状態です。/v1/models は実際に qwen3.8-27b-uncensored@iq4_xs などの一覧を返しました。

設定ファイルを見ると理由がはっきりします。

grep -E '"(networkInterface|cors|logSensitiveData|justInTimeModelLoading|port)"' \
  ~/.lmstudio/.internal/http-server-config.json
  "port": 1234,
  "cors": true,
  "logSensitiveData": true,
  "networkInterface": "0.0.0.0",
  "justInTimeModelLoading": true,

ここが肝で、lms server start --help の説明文はこうなっています。

--bind <address> Network address to bind the server to. Use "0.0.0.0" to accept connections from the local network, or "127.0.0.1" (default) for localhost only.

CLI の既定は 127.0.0.1 だと書いてあるのに、実際に動いているのは 0.0.0.0 でした。GUI 側のトグルで永続化された networkInterface が、autoStartOnLaunch: true の自動起動時に使われるためです。CLI のヘルプだけ読んで「既定で localhost だから大丈夫」と判断すると、ここで食い違います。

同時に気になる値が 2 つあります。

  • logSensitiveData: true … プロンプト本文がサーバーログに残る設定。無認証で投げられたリクエストの内容も含めて記録される。
  • justInTimeModelLoading: true … ロード済みモデルが無くても、リクエストが来た時点でモデルを読み込む。32GB 機で 16GB クラスのモデルがロードされるとメモリ余裕が一気に消える。

2. 公開範囲の点検スクリプト(コピペ可・読み取りのみ)

毎回手で打つのが面倒なので、点検だけするスクリプトにしました。設定を変更しません。自分のマシンに対してだけ実行してください。

cat > ~/llm-exposure-check.sh <<'EOF'
#!/bin/sh
# ローカルLLMサーバーの公開範囲を点検する(読み取りのみ・変更しない)
PORTS="1234 11434 8080 5000 3000 8000"

echo "== 1. ワイルドカード待ち受けのポート =="
for p in $PORTS; do
  out=$(lsof -nP -iTCP:"$p" -sTCP:LISTEN 2>/dev/null | tail -n +2)
  [ -z "$out" ] && continue
  echo "$out" | while read -r line; do
    addr=$(echo "$line" | awk '{print $(NF-1)}')
    cmd=$(echo "$line" | awk '{print $1}')
    case "$addr" in
      \*:*|0.0.0.0:*|\[::\]:*) echo "  [要確認] $p $cmd -> $addr (全インターフェース)";;
      127.0.0.1:*|\[::1\]:*)   echo "  [OK]     $p $cmd -> $addr (localhostのみ)";;
      *)                       echo "  [個別]   $p $cmd -> $addr";;
    esac
  done
done

echo "== 2. 自分のLAN IP 経由で応答するか =="
LANIP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null)
if [ -n "$LANIP" ]; then
  for p in $PORTS; do
    code=$(curl -s -m 2 -o /dev/null -w '%{http_code}' "http://$LANIP:$p/" 2>/dev/null)
    [ "$code" != "000" ] && echo "  [要確認] http://$LANIP:$p/ が応答 (HTTP $code)"
  done
else
  echo "  LAN IP を取得できず(スキップ)"
fi

echo "== 3. macOS アプリケーションファイアウォール =="
/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null \
  | sed 's/^/  /' || echo "  取得できず"

echo "== 4. LM Studio サーバー設定 =="
CFG="$HOME/.lmstudio/.internal/http-server-config.json"
if [ -f "$CFG" ]; then
  grep -E '"(networkInterface|cors|logSensitiveData|justInTimeModelLoading|port)"' "$CFG" | sed 's/^/  /'
else
  echo "  設定ファイルなし(LM Studio 未使用)"
fi
EOF
chmod +x ~/llm-exposure-check.sh
sh ~/llm-exposure-check.sh

自分の環境での実行結果がこれです。LLM サーバー以外も引っかかりました。

== 1. ワイルドカード待ち受けのポート ==
  [要確認] 1234 LM -> *:1234 (全インターフェース)
  [要確認] 5000 ControlCe -> *:5000 (全インターフェース)
  [要確認] 3000 com.docke -> *:3000 (全インターフェース)
== 2. 自分のLAN IP 経由で応答するか ==
  [要確認] http://192.168.0.92:1234/ が応答 (HTTP 200)
  [要確認] http://192.168.0.92:5000/ が応答 (HTTP 403)
  [要確認] http://192.168.0.92:3000/ が応答 (HTTP 200)
== 3. macOS アプリケーションファイアウォール ==
  Firewall is disabled. (State = 0)
== 4. LM Studio サーバー設定 ==
    "port": 1234,
    "cors": true,
    "logSensitiveData": true,
    "networkInterface": "0.0.0.0",
    "justInTimeModelLoading": true,

読み方のメモ:

  • 5000 の ControlCe は macOS の AirPlay レシーバー(コントロールセンター)です。OS 標準の挙動なので、LLM の話とは切り分けます。403 が返るのは想定どおり(ポート指定なしで lsof を流すと 7000 も同じプロセスで出てきます)。
  • 3000 の com.docke は Docker Desktop です。docker run -p 3000:3000 のようにホスト側のアドレスを省略して publish すると 0.0.0.0 に開くため、こうなります。-p 127.0.0.1:3000:3000 と書けば localhost 限定になります。
  • そして ファイアウォールが無効でした。ワイルドカード待ち受け + FW 無効 + 無認証、が重なっていたのが実態です。

3. bind 先を変えて実測した結果

lms server start --bind <addr> で待ち受け先を変えられます。3 パターン試して、どこから届くかを実測しました。

この 3 方向のうち、どれが通るかが bind 先で変わります。

--bind の指定 127.0.0.1 から LAN IP から Tailscale IP から 用途
0.0.0.0 届く (200) 届く (200) 届く (200) 広すぎる。既定にしない
127.0.0.1 届く (200) 届かない (接続拒否) 届かない 同じ Mac 内だけで使うなら最良
tailnet IP (Tailscale) 届かない 届かない 届く (200) 他端末から使いたいとき。ただし下の落とし穴あり

表の全セルは curl の応答コードで実際に確認したものです(http=000 は接続不可)。推測で埋めたセルはありません。

実際の確認コマンドと結果(127.0.0.1 に絞った場合):

lms server start --bind 127.0.0.1
lsof -nP -iTCP:1234 -sTCP:LISTEN | tail -1
curl -s -m 4 -o /dev/null -w "lo:  http=%{http_code}\n" http://127.0.0.1:1234/v1/models
curl -s -m 4 -o /dev/null -w "LAN: http=%{http_code}\n" http://192.168.0.92:1234/v1/models
Success! Server is now running on port 1234
LM\x20Stu 32818 ... TCP 127.0.0.1:1234 (LISTEN)
lo:  http=200
LAN: http=000     <- curl exit 7(接続できない)= 塞がった

lsof の表示が *:1234 から 127.0.0.1:1234 に変わり、LAN 経由は接続拒否になりました。これが期待どおりの状態です。

なお --bind 0.0.0.0 を指定すると CLI が警告を出します。これは正しい警告なので、出たら意図を確認してください。

W Server will accept connections from the network. Only use this if you know what you are doing!

つまずきポイント

tailnet IP に bind すると 127.0.0.1 からも届かなくなる

「LAN には開けたくないが、Tailscale 経由で他の端末からは使いたい」と考えて、bind 先を Tailscale の tailnet IP にしました。狙いどおり LAN からは塞がったのですが、同じ Mac 上の 127.0.0.1 からも届かなくなりました。

tailnet: http=200
LAN:     http=000
lo:      http=000     <- 同じマシン内からも繋がらない

特定 IP に bind すると、その 1 つの IP でしか待ち受けないので当然ではあります。ただ、同じ Mac で動かしている自作スクリプトや他のツールが http://127.0.0.1:1234/v1 を向いていると、それらが一斉に動かなくなります。自分はこれで一度ハマりました。

同じマシン内からも使いたいなら、素直に 127.0.0.1 に bind して、外からのアクセスは SSH ポートフォワードで持ってくるほうが素直です。

# 別の端末から。Mac 側は 127.0.0.1 に bind したまま使える
ssh -L 1234:127.0.0.1:1234 user@mac-mini

lsof でポートが出てこないのに外から届く

lsof -iTCP:8787 のようにポートを指定して探す方式は、調べるポートを列挙し忘れると漏れます。自分の環境では Docker が *:8787 でも待ち受けていましたが、スクリプトの PORTS に入れ忘れていて検出できていませんでした。網羅したいならポート指定なしで一覧し、ワイルドカードだけ抜くほうが確実です。

lsof -nP -iTCP -sTCP:LISTEN | awk 'NR==1 || /\*:|0\.0\.0\.0:/'

CORS を有効にしていると、閲覧中のサイトから叩かれうる

cors: true は Web アプリ開発には必要ですが、CLI のヘルプにも「Allows any website you visit to access the server」と書かれています。ブラウザで開いたページの JavaScript から http://127.0.0.1:1234 に到達できるということなので、開発で必要なときだけ有効にするのが無難です。

まとめ

  • ローカル LLM が安全なのは「サーバーが自分の手元だけを向いているとき」。待ち受けアドレスを一度は自分の目で確認する価値がある。
  • LM Studio は CLI の既定 (127.0.0.1) と GUI で永続化された設定 (0.0.0.0) が食い違いうる。ヘルプの既定値ではなく lsof の実測を信じる。
  • 点検は 4 項目。①lsof でワイルドカード待ち受けを探す ②自分の LAN IP に curl して応答コードを見る ③ファイアウォールの状態を見る ④http-server-config.json の networkInterface / cors / logSensitiveData を読む。
  • 塞ぐなら --bind 127.0.0.1。他端末から使いたいなら SSH ポートフォワードか VPN 経由にして、API 自体は localhost に閉じておく。
  • Docker の -p 3000:3000 も既定で 0.0.0.0 に開く。LLM だけの話ではないので、同じスクリプトでまとめて点検すると効率がよい。

未検証・正直に書いておくこと

  • Ollama については未検証です。この環境に Ollama を入れていないため、lsof で確認できていません。既定で 127.0.0.1:11434 に bind し、OLLAMA_HOST で変更する、という挙動は二次情報(個人ブログ・解説記事)で一致していますが、公式ドキュメントでの裏取りはできていません。使っている方は上のスクリプトの PORTS に 11434 が入っているので、自分の環境で確認してください。
  • 上の表の「届く/届かない」は自分の 1 台での実測です。LM Studio のバージョンやネットワーク構成で変わる可能性があります。
  • justInTimeModelLoading: true で「リクエストだけでモデルロードが発火する」のは設定の意味からの推論で、無認証リクエストで実際にロードが走るところまでは試していません(この日はモデルの実体が未マウントの外部ストレージ上にあり、ロード自体ができませんでした)。
  • macOS のアプリケーションファイアウォールを有効にした場合に、この種のアプリがどう扱われるかは試していません。

次に読む / 続編(CTA)

ローカルで動かすものの設定を点検する、という同じ発想の記事を書いています。あわせてどうぞ。

次は justInTimeModelLoading を有効にしたまま無認証アクセスを受けたときのメモリ挙動を、モデルを手元に置いた状態で測る予定です。上の点検スクリプトは手元に置いておくと新しいツールを入れたときにすぐ回せるので、ストックしておくと便利かもしれません。

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?