サーバーのライフサイクル: 参加と削除
一整套スクリプトで「VPSの購入 → Grafanaダッシュボードにメトリクスが表示されるまで」を1つのコマンドに圧縮します。参加 = bootstrap(公開鍵の配置/ホスト名の設定/SSH設定)→ mTLS証明書の発行 → otel-collectorのデプロイ → DIRECTルールの設定 → 正常性の確認; 削除 = サービスの停止 → 証明書の失効 → ルールの削除。全体で冪等性を保ち、各ステップで独立した判断を行い、途中で失敗しても再試行可能です。本章ではスクリプトの完全なロジックと機密情報を伏せたソースコードを解説します。
1. 使用方法
./add-server.sh <IP> --region cn|us|jp|eu|sg [--port N] [--dry-run]
./add-server.sh <sshエイリアス> [表示名] [--dry-run]
./remove-server.sh <sshエイリアス> [fleet-name] [--purge] [--dry-run]
前提条件: ローカルマシンにCAが構築済み、中継用のratholeが準備完了、DNS otlp.liz6.com が解決可能であること。スクリプトは set -euo pipefail を使用しており、いずれかのステップで失敗すると即座に停止します。完了済みのステップは再実行時に自動的にスキップされます。
2. add-server.sh: 裸のIPからオンラインへ
スクリプトは2つのフェーズに分かれています: bootstrap(IPモードのみ、裸のIPをSSHエイリアスに変換)→ 監視参加(IPモードとエイリアスモードの両方で共通の6ステップ)。
2.1 Bootstrap: 裸のIPの前処理
入力としてIPが指定された場合、まず5つの小さなステップを実行して、リモートマシンをSSH管理下に収めます:
① リージョンの検出: curl ip-api.com → countryCodeのマッピング: CN→cn, JP→jp, US→us, SG→sg など。失敗した場合は手動で --region を指定するよう促します。
② 自動命名: ~/.ssh/config 内の Host li-<region>-node-<N> エントリをスキャンし、max(N)+1 を取得して新しい名前を生成します。まずIPに既存のエイリアスがあるか確認し、あればそれを再利用します。同じHostが同じIPを指している場合はスキップし、異なるIPを指している場合は警告します。
③ 公開鍵の配置: ssh -o PasswordAuthentication=no root@<IP> echo ok → 鍵認証が既に設定されていればスキップします。そうでなければ ssh-copy-id を実行します。
④ ホスト名の設定: ssh root@<IP> hostname で現在の値を確認し、既に目標の名前であればスキップします。そうでなければ hostnamectl set-hostname を実行します。
⑤ SSH設定の書き込み + 指紋の確認: Hostブロック(StrictHostKeyChecking accept-new を含む)を追加し、初回接続時には手動で指紋を確認する必要があります。
_assign_name() {
local region="$1" ip="$2"
local existing; existing=$(grep -B5 "$ip" ~/.ssh/config 2>/dev/null \
| grep '^Host ' | grep -v 'HostName' | sed 's/Host //' | head -1 || true)
if [[ -n "$existing" ]]; then echo "$existing"; return; fi
local max_n; max_n=$(grep -oP "Host li-${region}-node-\K\d+" ~/.ssh/config 2>/dev/null \
| sort -n | tail -1 || echo 0)
echo "li-${region}-node-$((max_n + 1))"
}
_deploy_key() {
if ssh -o ConnectTimeout=8 -o PasswordAuthentication=no -p "$PORT" "root@$IP" \
'echo ok' 2>/dev/null | grep -q ok; then
echo "鍵認証が既に設定されています、スキップします"; return
fi
ssh-copy-id -o ConnectTimeout=15 -p "$PORT" "root@$IP"
}
_set_hostname() {
local cur; cur=$(ssh root@$IP hostname 2>/dev/null | tr -d '\r\n' || true)
if [[ "$cur" == "$1" ]]; then echo "既に $1 です、スキップします"; return; fi
ssh root@$IP "hostnamectl set-hostname '$1'"
}
2.2 監視参加 6ステップ(IPモードとエイリアスモード共通)
set -euo pipefail を使用し、各ステップは冪等性を持ちます。スクリプトの冒頭で入力タイプを判断します:
_is_ip() { [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; }
if _is_ip "$INPUT"; then
BOOTSTRAP=1 else
HOST="$INPUT"; BOOTSTRAP=0 fi
1/6 クライアント証明書の発行
CERTD="$CA_DIR/out/clients/$FLEET_NAME"
if [[ -d "$CERTD" ]]; then
echo "証明書が既に存在します、スキップします"
else
"$CA_DIR/issue-client-cert.sh" "$FLEET_NAME" fi
2/6 リモートアーキテクチャの検出 + バイナリのダウンロード
ARCH=$(ssh $HOST uname -m)
case "$ARCH" in
x86_64|amd64) GOARCH=amd64;;
aarch64|arm64) GOARCH=arm64;;
esac
3/6 mihomo DIRECTルール(ローカルマシン + N100)
重要な防御的なロジック——リモートVPSのパブリックIPは、ローカルマシンとN100の両方のmihomo DIRECTルールに追加する必要があります。そうしないと、SSHやOTLPの接続がTUNプロキシを通過してしまいます:
resolve_host_ip() {
local hostname; hostname=$(ssh -G "$1" 2>/dev/null | awk '/^hostname / {print $2; exit}')
if [[ "$hostname" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "$hostname"; return
fi
getent ahosts "$hostname" 2>/dev/null | awk '{print $1; exit}'
}
is_public_ip() {
local ip="$1"
[[ "$ip" =~ ^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.|169\.254\.|224\.) ]] && return 1
return 0
}
mihomo_add_direct() {
local ip="$1" desc="$2"
if ! grep -qF "${ip}/32" "$DESKTOP_MIHOMO_CONFIG" 2>/dev/null; then
sed -i "/255\.255\.255\.255\/32,DIRECT,no-resolve/a\ - IP-CIDR,${ip}/32,DIRECT,no-resolve # $desc" \
"$DESKTOP_MIHOMO_CONFIG"
curl -sf -X PUT "http://127.0.0.1:9999/configs?force=true" \
-H "Authorization: Bearer ${DESKTOP_MIHOMO_SECRET}" \
-d "{\"path\":\"${DESKTOP_MIHOMO_CONFIG}\"}" >/dev/null
fi
if ! ssh $N100_HOST "grep -qF '${ip}/32' $N100_PROFILE" 2>/dev/null; then
ssh $N100_HOST "sed -i '/255\.255\.255\.255\/32,DIRECT,no-resolve/a\ - IP-CIDR,${ip}/32,DIRECT,no-resolve # $desc' \
$N100_PROFILE && /etc/init.d/nikki reload" 2>/dev/null || \
echo "[!] N100に到達できません、後で手動で追加してください"
fi
}
両側で冪等性を確認: 既に存在すればスキップし、存在しなければ sed で 255.255.255.255/32,DIRECT ルールの下に挿入(ルール位置を統一)、その後サービス再起動なしでAPIホットリロードを行います。
4/6 バイナリ + 設定 + 証明書のプッシュ
scp "$BIN" "$HOST:/usr/local/bin/otelcol-contrib"
scp "$TEMPLATE_DIR/config.yaml" "$HOST:/etc/otelcol-fleet/config.yaml"
scp "$CERTD/client.crt" "$CERTD/client.key" "$CERTD/ca.crt" "$HOST:/etc/otelcol-fleet/"
ssh $HOST 'chmod 0600 /etc/otelcol-fleet/client.key'
ssh $HOST "cat > /etc/otelcol-fleet/env" <<EOF
FLEET_HOSTNAME=$FLEET_NAME
FLEET_OTLP_ENDPOINT=$OTLP_ENDPOINT
FLEET_COLLECT_INTERVAL=$FLEET_COLLECT_INTERVAL
EOF
config.yamlはテンプレートであり、envを使用してエンドポイント/ホスト名を注入します——複数のデプロイで同じテンプレート + 異なるenvを使用します。
5/6 systemdのインストールと起動
scp "$TEMPLATE_DIR/otelcol-fleet.service" "$HOST:/etc/systemd/system/"
ssh $HOST 'systemctl daemon-reload && systemctl enable --now otelcol-fleet'
ユニット設計: host_metrics は /proc//sys を読む必要があるため、User=root が最も簡単です。Restart=on-failure + RestartSec=5 を使用。RuntimeDirectory + StateDirectory でディレクトリを自動作成します。
6/6 正常性の確認
sleep 35 N=$(curl -s "http://127.0.0.1:9090/api/v1/query" \
--data-urlencode "query=count({host_name=\"$FLEET_NAME\"})" \
| grep -oP '"value":\[[^]]*,"\K[0-9]+' | head -1 || true)
if [[ -n "$N" && "$N" -gt 0 ]]; then
echo "[+] 完了! Prometheusは $N 件のシリアルを受信しました。Grafanaのドロップダウンに $FLEET_NAME が自動的に表示されます。"
else
echo "[!] メトリクスを受信していません。確認: ssh $HOST journalctl -u otelcol-fleet -n50"
fi
2.3 一般的な失敗例
| ステップ | 症状 | 原因 | 解決策 |
| 2/6 | SSHタイムアウト | mihomoのエグジットが一時的に海外ルートを迂回 | 数秒待ってから再試行 |
| 5/6 | active(auto-restart) | キューディレクトリが作成されていない | 修正済み: create_directory:true |
| 6/6 | activeだがシリアル=0 | TLSハンドシェイクの失敗 / DNS不通 | journalctl -u otelcol-fleet でOTLPのエラーを確認 |
3. remove-server.sh: 完全な削除
./remove-server.sh <ssh-host> [fleet-name] [--purge]
--purge を指定すると、リモートのバイナリとディスク上のキューも追加で削除されます。fleet-name を指定しない場合、SSH設定のHostNameから逆引きします。
1/4 サービスの停止と設定の削除
ssh $HOST 'systemctl disable --now otelcol-fleet 2>/dev/null || true
rm -f /etc/systemd/system/otelcol-fleet.service
rm -rf /etc/otelcol-fleet
systemctl daemon-reload'
2/4 証明書の失効
"$CA_DIR/revoke-client-cert.sh" "$NAME"
3/4 DIRECTルールの削除(両側で冪等)
mihomo_remove_direct() {
local ip="$1"
grep -qF "${ip}/32" "$DESKTOP_MIHOMO_CONFIG" 2>/dev/null || return
sed -i "/${ip//./\\.}\\/32.*DIRECT,no-resolve/d" "$DESKTOP_MIHOMO_CONFIG"
curl -sf -X PUT "http://127.0.0.1:9999/configs?force=true" \
-H "Authorization: Bearer ${DESKTOP_MIHOMO_SECRET}" \
-d "{\"path\":\"${DESKTOP_MIHOMO_CONFIG}\"}" >/dev/null
}
4/4 SSH記録のクリーンアップ
sed -i '/^Host '"$name"'$/,/^[^[:space:]]/d' ~/.ssh/config
Prometheusの履歴データ
既存のシリアルはstaleness(5分)により自動的に期限切れとなり、Grafanaのドロップダウンから自動的に消えます。即座に削除する必要がある場合は、admin APIを有効にして POST /api/v1/admin/tsdb/delete_series を実行します。
4. 関連ファイル
otelcol-fleet.service
[Unit]
Description=OpenTelemetry Collector (metrics-fleet agent)
After=network-online.target
[Service]
Type=simple
EnvironmentFile=/etc/otelcol-fleet/env
ExecStart=/usr/local/bin/otelcol-contrib --config /etc/otelcol-fleet/config.yaml
Restart=on-failure
RestartSec=5
User=root
RuntimeDirectory=otelcol-fleet
StateDirectory=otelcol-fleet
[Install]
WantedBy=multi-user.target
host_metrics は /proc//sys を読む必要があるため、User=root が最も簡単です。権限を降下させる場合は、CAP_SYS_PTRACE + CAP_DAC_READ_SEARCH を追加で設定する必要があります。
命名規則
| プレフィックス | リージョン | 番号 |
li-home- | ホーム | li-home-0(ローカルマシン), li-home-1(Windows), li-home-router(N100のサイドルート) |
li-cn-node- | 中国国内 | N |
li-us-node- | 米国 | N |
li-jp-node- | 日本 | N |
現在の対応状況: ホーム3台(ローカルマシン/Windows/N100)、中国国内2台(旧中継+腾讯云)、米国3台(VPS)、日本1台。
5. 付録: 完全なスクリプト
add-server.sh
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"
source ./fleet.env
source ./mihomo-sync.sh
DRY_RUN=0; PORT=22; REGION="auto"
INPUT=""; FLEET_NAME=""
for a in "$@"; do
case "$a" in
--port) shift; PORT="$1";;
--region) shift; REGION="$1";;
--dry-run) DRY_RUN=1;;
-*) echo "[!] 不明なオプション: $a" >&2; exit 1;;
*) [[ -z "$INPUT" ]] && INPUT="$a" || FLEET_NAME="$a";;
esac
done
[[ -n "$INPUT" ]] || { echo "使用方法: add-server.sh <IP|sshエイリアス> [表示名] [--port N] [--region auto|cn|us|jp]" >&2; exit 1; }
_is_ip() { [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; }
if _is_ip "$INPUT"; then
IP="$INPUT"; BOOTSTRAP=1
else
HOST="$INPUT"; BOOTSTRAP=0
fi
FLEET_NAME="${FLEET_NAME:-$INPUT}"
_deploy_key() {
echo "==> 公開鍵の配置"
if ssh -o ConnectTimeout=8 -o PasswordAuthentication=no -p "$PORT" "root@$IP" \
'echo ok' 2>/dev/null | grep -q ok; then
echo " 鍵認証が既に設定されています、スキップします"; return
fi
ssh-copy-id -o ConnectTimeout=15 -p "$PORT" "root@$IP"
echo " 公開鍵のデプロイ完了"
}
_set_hostname() {
local name="$1"
echo "==> リモートホスト名の設定: $name"
local cur
cur=$(ssh -o ConnectTimeout=10 -p "$PORT" "root@$IP" 'hostname' 2>/dev/null | tr -d '\r\n' || true)
if [[ "$cur" == "$name" ]]; then echo " 既に $name です、スキップします"; return; fi
ssh -o ConnectTimeout=10 -p "$PORT" "root@$IP" "hostnamectl set-hostname '$name'"
echo " $name に設定しました"
}
_add_ssh_config() {
local name="$1" ip="$2" port="$3"
echo "==> ~/.ssh/config の同期"
if grep -q "^Host $name$" ~/.ssh/config 2>/dev/null; then
local existing_ip; existing_ip=$(grep -A5 "^Host $name$" ~/.ssh/config | awk '/HostName / {print $2; exit}')
if [[ "$existing_ip" == "$ip" ]]; then
echo " Host $name → $ip は既に存在します、スキップします"; return
else
echo " [!] Host $name は存在しますが $existing_ip を指しています($ip ではありません)、書き込みをスキップします"; return
fi
fi
cat >> ~/.ssh/config <<EOF
# metrics-fleet $(date +%Y-%m-%d)
Host $name
HostName $ip
Port $port
User root
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking accept-new
EOF
echo " Host $name ($ip:$port) を追記しました"
}
_assign_name() {
local region="$1" ip="$2"
local existing; existing=$(grep -B5 "$ip" ~/.ssh/config 2>/dev/null \
| grep '^Host ' | grep -v 'HostName' | sed 's/Host //' | head -1 || true)
if [[ -n "$existing" ]]; then echo "$existing"; return; fi
local max_n; max_n=$(grep -oP "Host li-${region}-node-\K\d+" ~/.ssh/config 2>/dev/null \
| sort -n | tail -1 || echo 0)
echo "li-${region}-node-$((max_n + 1))"
}
if [[ "$BOOTSTRAP" == 1 ]]; then
echo "=== Bootstrap: 裸のIPからの開始 ==="
[[ "$REGION" != "auto" ]] || { echo "[!] IPモードでは --region の指定が必要です" >&2; exit 1; }
HOST=$(_assign_name "$REGION" "$IP")
echo " リージョン: $REGION | エイリアス: $HOST"
FLEET_NAME="${FLEET_NAME:-$HOST}"
_deploy_key
_set_hostname "$HOST"
_add_ssh_config "$HOST" "$IP" "$PORT"
ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 "$HOST" 'echo ok' >/dev/null
else
echo "=== SSHエイリアスモード: $HOST ==="
FLEET_NAME="${FLEET_NAME:-$HOST}"
fi
if [[ "$DRY_RUN" == 1 ]]; then
echo; echo "[dry-run] 実行予定: 証明書の発行 + アーキテクチャの検出 + DIRECT + プッシュ + systemd + 正常性確認"
exit 0
fi
SSH="ssh -o ConnectTimeout=10 $HOST"
OTLP_ENDPOINT="https://${RELAY_HOST}:${RELAY_PORT}${OTLP_PATH}"
echo "==> [1/6] クライアント証明書の発行: $FLEET_NAME"
CERTD="$CA_DIR/out/clients/$FLEET_NAME"
if [[ -d "$CERTD" ]]; then echo " 証明書が既に存在します、スキップします"
else "$CA_DIR/issue-client-cert.sh" "$FLEET_NAME"; fi
echo "==> [2/6] リモートアーキテクチャの検出"
ARCH="$($SSH uname -m)"
case "$ARCH" in
x86_64|amd64) GOARCH=amd64;;
aarch64|arm64) GOARCH=arm64;;
*) echo "[!] サポートされていないアーキテクチャ: $ARCH" >&2; exit 1;;
esac
echo "==> [3/6] mihomo DIRECTルール(ローカルマシン + N100)"
REMOTE_IP=$(resolve_host_ip "$HOST")
if [[ -n "$REMOTE_IP" ]] && is_public_ip "$REMOTE_IP"; then
mihomo_add_direct "$REMOTE_IP" "metrics-fleet:$FLEET_NAME"
else
echo " プライベートネットワーク/解決不可、DIRECTをスキップします"
fi
echo "==> [4/6] バイナリ + 設定 + 証明書のプッシュ"
DISTFILES="$HOME/distfiles"
BIN="$DISTFILES/otelcol-contrib_${OTELCOL_VERSION}_linux_${GOARCH}"
if [[ ! -x "$BIN" ]]; then
mkdir -p "$DISTFILES"
TARURL="https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${OTELCOL_VERSION}/otelcol-contrib_${OTELCOL_VERSION}_linux_${GOARCH}.tar.gz"
tmp="$(mktemp -d)"
curl -fSL "$TARURL" -o "$tmp/o.tgz"
tar -xzf "$tmp/o.tgz" -C "$tmp" otelcol-contrib
mv "$tmp/otelcol-contrib" "$BIN"
chmod +x "$BIN"; rm -rf "$tmp"
fi
$SSH 'install -d -m 0750 /etc/otelcol-fleet'
scp -q "$BIN" "$HOST:/usr/local/bin/otelcol-contrib"
scp -q "$HERE/../otel/config.yaml" "$HOST:/etc/otelcol-fleet/config.yaml"
scp -q "$CERTD/client.crt" "$CERTD/client.key" "$CERTD/ca.crt" "$HOST:/etc/otelcol-fleet/"
$SSH 'chmod 0600 /etc/otelcol-fleet/client.key'
$SSH "cat > /etc/otelcol-fleet/env" <<EOF
FLEET_HOSTNAME=$FLEET_NAME
FLEET_OTLP_ENDPOINT=$OTLP_ENDPOINT
FLEET_COLLECT_INTERVAL=$FLEET_COLLECT_INTERVAL
EOF
echo "==> [5/6] systemdサービスのインストールと起動"
scp -q "$HERE/../otel/otelcol-fleet.service" "$HOST:/etc/systemd/system/otelcol-fleet.service"
$SSH 'systemctl daemon-reload && systemctl enable --now otelcol-fleet'
echo "==> [6/6] 正常性の確認"
sleep 3
if $SSH 'systemctl is-active --quiet otelcol-fleet'; then echo " リモートサービス: active"
else echo "[!] 起動していません: ssh $HOST journalctl -u otelcol-fleet -n50" >&2; exit 1; fi
echo " メトリクスが自宅に届くのを待っています..."
sleep 35
N="$(curl -s "http://127.0.0.1:9090/api/v1/query" \
--data-urlencode "query=count({host_name=\"$FLEET_NAME\"})" \
| grep -oP '"value":\[[^]]*,"\K[0-9]+' | head -1 || true)"
if [[ -n "${N:-}" && "$N" -gt 0 ]]; then
echo "[+] 完了! Prometheusは $N 件のシリアルを受信しました。Grafanaのドロップダウンに $FLEET_NAME が自動的に表示されます。"
else
echo "[!] リモートは起動していますがメトリクスを受信していません。確認: ssh $HOST journalctl -u otelcol-fleet -n50"
fi
remove-server.sh
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"
source ./fleet.env
source ./mihomo-sync.sh
HOST="${1:?使用方法: remove-server.sh <ssh-host> [fleet-name] [--purge] [--dry-run]}"
NAME="$HOST"; PURGE=0; DRY_RUN=0
shift
for a in "$@"; do
case "$a" in
--purge) PURGE=1;;
--dry-run) DRY_RUN=1;;
*) NAME="$a";;
esac
done
SSH="ssh -o ConnectTimeout=10 $HOST"
IP=$(resolve_host_ip "$HOST")
_remove_ssh_config() {
local name="$1"
if ! grep -q "^Host $name$" ~/.ssh/config 2>/dev/null; then
echo " ~/.ssh/config: Host $name が見つかりません、スキップします"; return
fi
sed -i '/^Host '"$name"'$/,/^[^[:space:]]/{/^Host '"$name"'$/d;/^[[:space:]]/d}' ~/.ssh/config
echo " ~/.ssh/config から Host $name を削除しました"
}
echo "=== サーバーの削除: $NAME ($HOST) ==="
echo "==> [1/4] リモートcollectorの停止とアンインストール"
if [[ "$DRY_RUN" == 0 ]]; then
$SSH 'systemctl disable --now otelcol-fleet 2>/dev/null || true
rm -f /etc/systemd/system/otelcol-fleet.service
rm -rf /etc/otelcol-fleet
systemctl daemon-reload' 2>/dev/null || true
echo " collectorを停止し、設定をアンインストールしました"
if [[ "$PURGE" == 1 ]]; then
$SSH 'rm -f /usr/local/bin/otelcol-contrib; rm -rf /var/lib/otelcol-fleet' 2>/dev/null || true
echo " バイナリとキューをpurgeしました"
fi
fi
echo "==> [2/4] クライアント証明書の失効: $NAME"
[[ "$DRY_RUN" == 0 ]] && "$CA_DIR/revoke-client-cert.sh" "$NAME"
echo "==> [3/4] mihomo DIRECTルールの削除"
if [[ -n "$IP" ]] && is_public_ip "$IP"; then
mihomo_remove_direct "$IP"
else
echo " プライベートネットワーク/解決不可、スキップします"
fi
echo "==> [4/4] SSH記録のクリーンアップ"
_remove_ssh_config "$NAME"
ssh-keygen -R "$NAME" 2>/dev/null || true
echo "[+] $NAME は完全に削除されました (collector + 証明書 + DIRECT + SSH記録)。"
echo " 既存のメトリクスはstalenessにより自動的に期限切れになります。即座に削除する必要がある場合: Prometheus admin API delete_series。"
mihomo-sync.sh(DIRECTルール同期補助)
resolve_host_ip() {
local hostname; hostname=$(ssh -G "$1" 2>/dev/null | awk '/^hostname / {print $2; exit}')
[[ -z "$hostname" ]] && { echo ""; return; }
if [[ "$hostname" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "$hostname"; return
fi
getent ahosts "$hostname" 2>/dev/null | awk '{print $1; exit}'
}
is_public_ip() {
local ip="$1"
[[ -z "$ip" ]] && return 1
[[ "$ip" =~ ^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.|169\.254\.|224\.) ]] && return 1
return 0
}
mihomo_add_direct() {
local ip="$1" desc="${2:-}"
local comment=""; [[ -n "$desc" ]] && comment=" # $desc"
local line=" - IP-CIDR,${ip}/32,DIRECT,no-resolve${comment}"
if ! grep -qF "${ip}/32" "$DESKTOP_MIHOMO_CONFIG" 2>/dev/null; then
echo "$SUDO_PASSWORD" | sudo -S sed -i \
"/255\.255\.255\.255\/32,DIRECT,no-resolve/a\\${line}" "$DESKTOP_MIHOMO_CONFIG"
curl -sf -X PUT "http://127.0.0.1:9999/configs?force=true" \
-H "Authorization: Bearer ${DESKTOP_MIHOMO_SECRET}" \
-d "{\"path\":\"${DESKTOP_MIHOMO_CONFIG}\"}" >/dev/null \
&& echo " ローカルマシン: ${ip}/32 DIRECTを追加しました" || echo " ローカルマシン: 設定は書き込み済み(ホットリロードに失敗)"
else echo " ローカルマシン: ${ip}/32 は既に存在します、スキップします"; fi
if ! ssh -o ConnectTimeout=8 "$N100_HOST" "grep -qF '${ip}/32' $N100_PROFILE" 2>/dev/null; then
ssh -o ConnectTimeout=8 "$N100_HOST" \
"sed -i \"/255\\.255\\.255\\.255\\/32,DIRECT,no-resolve/a\\${line}\" $N100_PROFILE \
&& (/etc/init.d/nikki reload 2>/dev/null || true)" 2>/dev/null \
&& echo " N100: ${ip}/32 DIRECTを追加しました" || echo " [!] N100に到達できません、後で手動で追加してください"
else echo " N100: ${ip}/32 は既に存在します、スキップします"; fi
}
mihomo_remove_direct() {
local ip="$1"; local escaped; escaped=$(echo "$ip" | sed 's/\./\\./g')
if grep -qF "${ip}/32" "$DESKTOP_MIHOMO_CONFIG" 2>/dev/null; then
echo "$SUDO_PASSWORD" | sudo -S sed -i "/${escaped}\\/32.*DIRECT,no-resolve/d" "$DESKTOP_MIHOMO_CONFIG"
curl -sf -X PUT "http://127.0.0.1:9999/configs?force=true" \
-H "Authorization: Bearer ${DESKTOP_MIHOMO_SECRET}" \
-d "{\"path\":\"${DESKTOP_MIHOMO_CONFIG}\"}" >/dev/null \
&& echo " ローカルマシン: ${ip}/32 を削除しました" || echo " ローカルマシン: 設定は書き込み済み(ホットリロードに失敗)"
fi
if ssh -o ConnectTimeout=8 "$N100_HOST" "grep -qF '${ip}/32' $N100_PROFILE" 2>/dev/null; then
ssh -o ConnectTimeout=8 "$N100_HOST" \
"sed -i \"/${escaped}\\/32.*DIRECT,no-resolve/d\" $N100_PROFILE \
&& (/etc/init.d/nikki reload 2>/dev/null || true)" 2>/dev/null \
&& echo " N100: ${ip}/32 を削除しました" || echo " [!] N100に到達できません、後で手動で削除してください"
fi
}
関連ドキュメント