クラウドMacで検証するiOSのIPアドレス解析と正規化

クラウドMacで検証するiOSのIPアドレス解析と正規化

Windowsで接続設定を変更し、クラウドMacで検証しようとしたところ、同じデバイスのIPv6アドレスが別々の接続先として扱われていました。一方はゼロの並びを省略せず、もう一方は二重コロンで省略しています。接続先一覧、重複排除、アクセスルールがそれぞれ元の文字列を比較していると、判定が食い違います。ここではSwiftとシステムの解析関数を使い、オフラインで実行できる検証を用意します。実際のサーバーは不要で、ネットワークの接続状態にも左右されません。

まず入力の範囲を決める

この例で受け付けるのはIPアドレスリテラルだけです。ドメイン名、ポート、完全なURLは対象にしません。IPv4はドット区切りの4つの10進数、IPv6はシステムが対応する標準的な表記を受け付けます。前後に空白がある入力はそのまま拒否し、暗黙にトリミングしません。画面に表示された値と実際の接続に使う値が食い違うのを防ぐためです。

IPv6を囲む角括弧はURLのホスト表記に使うもので、アドレス自体の一部ではありません。%en0のようなスコープ識別子も、このパーサーでは扱いません。リンクローカルアドレスに接続する必要がある場合は、別のレイヤーでアドレスとインターフェース情報を明示的に受け取ってください。パーセント記号以降を安易に切り捨て、そのまま接続してはいけません。

解析の成功が示すのは、そのテキストがアドレスを表せるということだけです。アクセスが許可されていることや、対応するデバイスがオンラインであることは保証しません。

サンプルに含まれるドキュメント用アドレスは、ローカルでの解析にのみ使用します。スクリプトがこれらのアドレスに接続することはありません。

アドレスファミリーとバイト列で結果を保持する

クラウドMacのプロジェクトディレクトリにip-contract.swiftを作成します。まずxcrun swiftc --versionでコンパイラー情報が表示されることを確認し、次のコードを保存してください。ホスト名を解決する可能性があるAPIは使わず、リテラルの検証にDNSの動作が混ざらないようにします。

import Darwin
import Foundation

struct IPAddress: Equatable {
    let family: Int32
    let bytes: [UInt8]
}

func parseIP(_ text: String) -> IPAddress? {
    guard !text.isEmpty,
          text.utf8.count <= 45,
          !text.utf8.contains(0) else {
        return nil
    }

    var v4 = in_addr()
    let result4 = text.withCString {
        inet_pton(AF_INET, $0, &v4)
    }
    if result4 == 1 {
        return IPAddress(
            family: AF_INET,
            bytes: withUnsafeBytes(of: &v4) { Array($0) }
        )
    }

    var v6 = in6_addr()
    let result6 = text.withCString {
        inet_pton(AF_INET6, $0, &v6)
    }
    if result6 == 1 {
        return IPAddress(
            family: AF_INET6,
            bytes: withUnsafeBytes(of: &v6) { Array($0) }
        )
    }

    return nil
}

func require(_ condition: Bool, _ message: String) {
    if !condition {
        fatalError(message)
    }
}

let good = [
    "192.0.2.1",
    "2001:db8::1",
    "2001:0DB8:0000:0000:0000:0000:0000:0001",
    "::",
    "::1",
    "::ffff:192.0.2.1"
]

for text in good {
    require(parseIP(text) != nil, "Rejected valid literal")
}

let bad = [
    "",
    " 192.0.2.1",
    "192.0.2.1\n",
    "192.0.2.256",
    "192.0.2",
    "192.0.02.1",
    "0xC0000201",
    "[2001:db8::1]",
    "fe80::1%en0",
    "2001:::1",
    "192.0.2.1\u{0}ignored"
]

for text in bad {
    require(parseIP(text) == nil, "Accepted invalid literal")
}

let short = parseIP("2001:db8::1")!
let expanded = parseIP(
    "2001:0DB8:0000:0000:0000:0000:0000:0001"
)!
require(short == expanded, "Equivalent IPv6 mismatch")
require(short.bytes.count == 16, "Invalid IPv6 length")

let v4 = parseIP("192.0.2.1")!
let mapped = parseIP("::ffff:192.0.2.1")!
require(v4.bytes.count == 4, "Invalid IPv4 length")
require(v4 != mapped, "Address families collapsed")

長さの上限は、このパーサーの入力仕様で定めたバイト数の制限です。スコープ付きアドレス全般に適用する制限ではありません。アドレスのバイト列はシステムの構造体から直接取得し、ネットワークバイトオーダーを維持します。このデータ全体をホストの整数として解釈し直して比較しないでください。

コンパイルして実行し、失敗時の挙動も確認する

スクリプトを単体で実行できるプログラムにコンパイルします。ビルドタスクに組み込みやすくなり、起動方法の違いがインタープリター実行に影響することも避けられます。

set -eu
mkdir -p .ip-check
xcrun swiftc ip-contract.swift -o .ip-check/ip-contract
.ip-check/ip-contract

すべての検証を通過すると何も出力せず、終了コードはゼロになります。仕様上の条件を1つでも満たさなければ、ゼロ以外のステータスで終了します。assertではなく明示的なチェックを使うのは、最適化設定によってアサーションが削除される可能性があるためです。

いったん等価比較を不等価比較に変更し、タスクが確実に失敗することを確認してから元に戻してください。この意図的な失敗テストによって、パイプラインが終了コードを無視していたり、ログを保存するだけで成功扱いにしていたりする問題を見つけられます。単に「一度実行できた」ことだけで確認を終えないでください。

正規化とアクセスポリシーを分ける

等価とみなす範囲を安易に広げない

同じIPv6アドレスの省略表記と完全表記は等しいと判定すべきですが、このモデルではIPv4とIPv4射影IPv6を異なるアドレスファミリーとして扱います。アプリケーションの要件として両者を同じ論理的な接続先にまとめる場合は、明示的な変換処理を追加し、変換前後のアクセスルールをテストしてください。文字列のプレフィックスを削除して済ませてはいけません。

表示にはinet_ntopを使い、統一したテキスト表記を出力できます。一方、重複排除とアクセス許可の判定には、引き続き解析後のモデルを使います。あるシステムバージョンが出力した文字列表現を、バージョンをまたいでも永久に変わらない保存形式として扱わないでください。

ドメイン名は別に検証する

ドメイン名がこの関数を通らないのは、無効だからではなく、この入力型の対象外だからです。製品でドメイン名もサポートするなら、専用の入力経路を用意し、名前解決後に実際の接続先となるアドレス群を検査する必要があります。ユーザーが入力したホスト文字列だけを確認しても、名前解決結果の変化には対応できません。

ループバック、プライベート、リンクローカルなどのアドレスをどこまで許可するかは、製品の用途に応じて決めます。デバイスのデバッグツールと、公開インターネット上のリソースを取得するサービスでは必要なルールが異なります。意味が曖昧な「安全なアドレス」という真偽値を共用すべきではありません。

失敗した入力を回帰テスト資産として残す

パーサーを共有ソースコードに移したら、macOSのコマンドライン検証とiOSのテストターゲットで、同じテストベクトルを実行します。前者はリモート環境で問題を素早く洗い出すのに適し、後者は対象OSでの挙動を確認する役割を担います。ホスト側での合格を、対象プラットフォームでの受け入れ検証の代わりにはできません。

修正のたびに、元の入力、期待するアドレスファミリー、期待する結果を追加してください。特に埋め込まれたヌルバイトは、元のバイト列を残す必要があります。ログ表示では見えなくなる可能性があるためです。C文字列はヌルバイトで終端するので、入口でチェックしないと、先頭にある有効な部分だけを検証してしまうおそれがあります。

最後に確認するのは3点です。異なる表記を同じアドレスとして扱えるか、異なるアドレスファミリーの境界を保てるか、不正な入力が接続前に失敗するか。この仕様が安定していれば、後からUIの入力検証や接続ポリシーを追加する際にも、繰り返し実行できる検証の基盤になります。

よくある質問

IPv6の文字列表記が違えば別のアドレスですか?

いいえ。ゼロの省略、先頭のゼロ、英字の大小で表記は変わります。解析後のアドレスファミリーとネットワークバイト列を比較してください。

inet_ptonで解析できれば接続を許可してよいですか?

いいえ。ループバック、プライベート、リンクローカル、IPv4射影アドレスなどの許可判断は別途必要です。名前解決後のアドレスも確認します。

専用のクラウドMacに作業を任せよう

専用の物理 Mac mini をレンタルして、用途に合わせてロケーションと利用期間を選べます。利用開始後は管理画面で接続情報を確認できます。ご利用中に問題が発生した場合は、管理画面からサポートチケットをお送りください。

クラウドMacを選ぶ

用途に合わせてロケーションを選ぶ

構成と利用期間を確認して、専用の物理 Mac mini をお申し込みください。