コンテンツにスキップ
OmniRoute source

Egress IP Family Policy (IPv4/IPv6) (日本語)


レジストリ内の各プロキシには、3つの値のいずれかを取るfamilyフィールドがあり、Zodの列挙型によって検証されます。

src/shared/validation/schemas.ts
family: z.enum(["auto", "ipv4", "ipv6"]).optional().default("auto"),

このフィールドのデフォルト値は"auto"であり、従来のデュアルスタック動作が維持されます。ipv4またはipv6に設定すると、そのプロキシの接続ファミリーが固定されます。

この指定は単一のヘルパーを通じてすべての場所で正規化されるため、未知の値はすべてautoに集約されます。

open-sse/utils/proxyFamily.ts
export type ProxyFamily = "auto" | "ipv4" | "ipv6";
export function parseProxyFamily(value: unknown): ProxyFamily {
return value === "ipv4" || value === "ipv6" ? value : "auto";
}

PR #3777で導入されました。導入の動機となった問題は次のとおりです。

問題 この指定による解決方法
IPv6専用エグレスからIPv4への漏洩 プロキシホストにAレコードとAAAAレコードの両方がある場合(またはOSがIPv4を優先する場合)、IPv6専用パスを意図していても、Happy EyeballsによってIPv4経由で外部に接続される可能性があります。ipv6に固定することで、その漏洩を防止できます。
共有エグレスの異常による失効 ローテーション型プロバイダー(codex/openai)は、多数のアカウントが高頻度で同じIPから外部に接続すると、トークンを失効させます。エグレスファミリーの制御は、アカウントごとに異なる予測可能なエグレスパスを維持するための一環です(これと組み合わせて使用するエグレスIP診断については、src/lib/proxyEgress.tsを参照してください)。
コンプライアンス/テスト向けの決定論的なエグレス トラフィックが特定のファミリー経由で送信されることを保証する必要がある場合、autoでは不十分です。

この指定は意図的にグローバルではなく、プロキシ単位で設定されます。プール内のプロキシごとに異なるポリシーを設定できます。


値 UIラベル 動作
auto 自動(デュアルスタック) OSがアドレスファミリーを選択します。プロキシホストがIPリテラルの場合、ファミリーはそのリテラルによって決まります。ホスト名の場合は、両方のファミリーが使用可能です。これがデフォルトです。
ipv4 IPv4のみ 接続をIPv4に固定します。プロキシホストにIPv4(A)レコードがない場合は、フェイルクローズします。
ipv6 IPv6のみ 接続をIPv6に固定します。プロキシホストにIPv6(AAAA)レコードがない場合は、フェイルクローズします。

UI文字列はsrc/i18n/messages/en.json(labelFamily、familyAuto、familyIpv4、familyIpv6、familyHint)にあります。


セレクターは、Proxy Poolタブのプロキシフォームにあります。

  1. Dashboard → Settings → Proxy → Proxy Poolを開きます
  2. プロキシを追加または編集します
  3. IPファミリードロップダウンを自動(デュアルスタック)、IPv4のみ、またはIPv6のみに設定します
  4. 保存します

このコントロールはProxyRegistryManager.tsxによってレンダリングされます(proxy/ProxyPoolTab.tsxにマウントされています)。

familyフィールドはプロキシレジストリの作成/更新ペイロードに含まれ、createProxyRegistrySchema / updateProxyRegistrySchema(src/shared/validation/schemas.ts)によって検証され、POST / PATCH /api/v1/management/proxiesによって処理されます。

ターミナルウィンドウ
# IPv6専用プロキシを作成
curl -X POST http://localhost:20128/api/v1/management/proxies \
-H "Content-Type: application/json" \
-d '{
"name": "IPv6 egress",
"type": "socks5",
"host": "proxy.example.com",
"port": 1080,
"family": "ipv6"
}'
# 既存のプロキシをIPv4専用に変更
curl -X PATCH http://localhost:20128/api/v1/management/proxies \
-H "Content-Type: application/json" \
-d '{ "id": "proxy-uuid-here", "family": "ipv4" }'

同じフィールドは、アップストリームプロキシエントリで使用されるインラインプロキシ設定オブジェクトでも受け付けられます(upstream_proxy_config.family。データモデルを参照)。

プロキシに関するその他のCRUD/割り当てAPIについては、PROXY_GUIDE.mdを参照してください。


familyがautoの場合、OmniRouteはどのディレクティブも追加しません。プロキシURLはそのまま使用され、接続ファミリーは固有の情報に基づいて決定されます。

URLの構築時(open-sse/utils/proxyDispatcher.tsのproxyConfigToUrl / normalizeProxyUrl)、autoプロキシはマーカーのないプレーンURLになります。

open-sse/utils/proxyDispatcher.ts
const fam = parseProxyFamily(config.family);
const normalized = normalizeProxyUrl(proxyUrlStr, "context proxy", {
allowSocks5,
});
return fam === "auto" ? normalized : `${normalized}?family=${fam}`;

ディスパッチ時(resolveDispatcherFamily)、autoはIPリテラルホスト固有のファミリーに解決されます。ホスト名の場合はnull(OSに決定を委ねる)に解決されます。

open-sse/utils/proxyDispatcher.ts
function resolveDispatcherFamily(parsed: URL): 4 | 6 | null {
const directive = parseProxyFamily(parsed.searchParams.get("family") ?? undefined);
const literal = detectIpLiteralFamily(parsed.hostname);
if (directive === "auto") return literal; // ホスト名の場合はnull → OSが選択
// ...
}

したがって、次のようになります。

  • auto + IPリテラルホスト(192.0.2.1 / [2001:db8::1])→ そのリテラルのファミリー。
  • auto + ホスト名 → null → 標準的なデュアルスタックOS名前解決。

auto 以外のディレクティブは、単一の合成クエリマーカー(?family=ipv4 または ?family=ipv6)として、正規化されたプロキシ URL に一度だけ追加されます。normalizeProxyUrl は、ポート解析を壊すことがないよう、このマーカーを正確に一度だけ削除してから再追加します。

ディスパッチャーの構築時に、このマーカーが読み取られ、具体的な接続ファミリーに変換されます。ホストが反対側のファミリーに属する IP リテラルの場合、OmniRoute は例外をスローします(矛盾がある場合はフェイルクローズします)。

open-sse/utils/proxyDispatcher.ts
const want = directive === "ipv6" ? 6 : 4;
if (literal !== null && literal !== want) {
throw new Error(
`[ProxyDispatcher] Proxy family directive ${directive} contradicts ${literal === 6 ? "IPv6" : "IPv4"} literal host`
);
}

その後、具体的なファミリーがコネクターに固定されます。

  • HTTP/HTTPS プロキシ(ProxyAgent):proxyTls: { family, autoSelectFamily: false } — Happy Eyeballs を無効化し、選択したファミリーだけが接続に使用されるようにします。
  • SOCKS5 プロキシ:カスタムコネクターが socket_options: { family, autoSelectFamily: false } を SOCKS クライアントに渡します(SOCKS5 の互換性を参照)。

ファミリーの固定は SOCKS5 プロキシでも機能しますが、標準の fetch-socks では、プロキシホップのファミリーを固定するために必要なソケットオプションが公開されていません。そのため、OmniRoute には独自のコネクターが含まれています。

open-sse/utils/socksConnectorWithFamily.ts
export function buildSocksFamilySocketOptions(family: 4 | 6 | null): Record<string, unknown> {
if (family === 6) return { family: 6, autoSelectFamily: false };
if (family === 4) return { family: 4, autoSelectFamily: false };
return {};
}

すべての SOCKS5 ディスパッチは、family の値にかかわらず(ホスト名に対する null / auto を含む)、createSocksDispatcherWithFamily を経由します。buildSocksFamilySocketOptions(null) は {} を返し、同じ SocksClient.createConnection + TLS buildConnector パスが socket_options の固定とともに使用されるため、IPv6 専用のエグレスポリシーに対して Happy Eyeballs が IPv4 を選択することはありません。

SOCKS5 サポート自体はデフォルトで有効です(ENABLE_SOCKS5_PROXY=false でオプトアウトできます)。PROXY_GUIDE.md → 環境変数を参照してください。


このディレクティブの目的は、誤ったファミリーへ暗黙的にフォールバックするのではなく、接続を拒否することです。次の 2 つのガードによって、これが保証されます。

  1. リテラルの矛盾 — IP リテラルのホストと矛盾するディレクティブは、ディスパッチャーの構築時に例外をスローします(上記の resolveDispatcherFamily)。

  2. ホスト名に対する事前 DNS チェック — ファミリーが固定されたホスト名プロキシの場合、proxyFetch.ts はエグレスを開始する前に、assertHostnameSupportsFamily を通じて、ホスト名に必要なファミリーのレコードが実際に存在することを確認します。

    open-sse/utils/proxyFamilyResolve.ts
    const hasFamily = records.some((r) => r.family === family);
    if (!hasFamily) {
    throw new Error(
    `[ProxyFamily] Proxy host ${host} has no ${family === 6 ? "IPv6 (AAAA)" : "IPv4 (A)"} record; ` +
    `refusing ${family === 6 ? "IPv6" : "IPv4"}-only egress (fail-closed)`
    );
    }

    失敗した場合、proxyFetch.ts はエラーに code = "PROXY_FAMILY_UNAVAILABLE" および statusCode = 503 を設定します。DNS 解決の失敗も同様にフェイルクローズとして処理され、エグレスが拒否されます。

IP リテラルのホストに対する事前 DNS チェックは何も行いません。ファミリーは IP リテラル自体に内在しており、ルックアップは不要です。


family カラムは、マイグレーション 099_proxy_family.sql によって 2つ のテーブルに追加されました。

-- src/lib/db/migrations/099_proxy_family.sql
ALTER TABLE proxy_registry ADD COLUMN family TEXT NOT NULL DEFAULT 'auto';
ALTER TABLE upstream_proxy_config ADD COLUMN family TEXT NOT NULL DEFAULT 'auto';
  • proxy_registry.family — レジストリエントリに対するプロキシ単位のディレクティブです(src/lib/db/proxies.ts)。解決クエリでは、ほかのプロキシカラムとともに family が選択され、値が存在しない場合や文字列でない場合は "auto" に変換されます。
  • upstream_proxy_config.family — アップストリームプロキシエントリに対するディレクティブです(src/lib/db/upstreamProxy.ts)。同様に、デフォルト値は "auto" です。

解決済みのプロキシオブジェクトが auto 以外の family を持つ場合、proxyConfigToUrl は ?family= マーカーを追加します。これにより、指定された設定がディスパッチャーまで確実に維持されます。


📖 関連ドキュメント:

  • プロキシガイド — レジストリの CRUD、4段階の解決、ローテーション、ヘルスチェック、API リファレンスを含むプロキシシステム全体
  • docs/security/STEALTH_GUIDE.md(git 内にあり、/docs にはコンパイルされません)— プロキシ上で動作する TLS フィンガープリントおよび CLI フィンガープリントの各レイヤー
  • ルートガードの階層 — ローカル専用ルートに対するループバック強制

OmniRoute ソースコード (a58000c7685f)

HagiCode

HagiCode は構造化ワークフロー、マルチエージェント実行、Hero Dungeon ビューを備えたエージェント型コーディングワークスペースです。

よりスマートで速く、楽しいエージェント型ワークフローで、使いやすいソフトウェアを形にします。

HagiCode ライトテーマのメイン画面
  • Smart構造化ワークフローは意図をアイデアから変更のリリースまで実行可能な道筋にします。
  • Efficientマルチエージェントのワークフローで調査、実装、レビューを並行して進めます。
  • FunHero Dungeon により長時間のコーディングを視覚的で協力的な体験にします。
HagiCode を見る