axiom-networking-diag について
接続、TLS、データ転送、パフォーマンスの問題を調べる、iOS開発者向けのNetwork.framework診断スキルです。状態やログを先に収集し、診断ツリーに沿って修正を一つずつ検証します。
主な機能
- 01接続状態に基づく診断
- 02DNSとTLSの障害に対応
- 03メッセージフレーミングの調査
- 04輻輳とバッファリングの診断
- 05検証・テスト手順を提示
活用シーン
- 01準備中や待機中で止まる接続の調査
- 02TLS証明書検証エラーの分析
- 03送信成功後にデータを受信できない問題の調査
- 04ネットワーク切り替え後の接続・性能問題の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-networking-diagインストール後、Claude Code で/axiom-networking-diagを入力して読み込まれたか確認します。
詳細
Network.framework の診断
概要
基本原則: ネットワークの問題の85%は、接続状態の誤解、ネットワークの切り替わりへの未対応、または不適切なエラー処理に起因します。Network.framework の欠陥が原因ではありません。
Network.framework はすべての iOS アプリで実績があり(内部で URLSession を支えています)、毎日何兆ものリクエストを処理し、Happy Eyeballs、プロキシ評価、WiFi Assist によるスマートな接続確立を提供します。接続に失敗したり、タイムアウトしたり、予期しない動作をしたりする場合、ほとんどの場合、問題はフレームワーク自体ではなく、フレームワークの使い方にあります。
このスキルでは、根本原因を数時間ではなく数分で特定するための体系的な診断方法を説明します。
危険信号 — ネットワークの問題を疑う
次のいずれかが見られる場合は、フレームワークの故障ではなく、ネットワークの設定ミスを疑ってください。
-
明確なエラーがないまま、接続が60秒後にタイムアウトする
-
一部のネットワークで「certificate invalid」と表示され、TLS ハンドシェイクに失敗する
-
データは送信されたが、受信側に届かない
-
WiFi からセルラーに切り替えると接続が切れる
-
WiFi では問題なく動作するが、セルラーでは毎回失敗する
-
シミュレーターでは動作するが、実機では失敗する
-
自分のネットワークでは接続できるが、ユーザーの環境では失敗する
-
❌ 禁止「Network.framework は壊れているので、ソケットを使って書き直すべきです」
- Network.framework はすべての iOS アプリで使われている URLSession を支えています
- ソケットを使う場合、発見するまで何カ月もかかるようなエッジケースにも対応しています
- Apple のエンジニアによる10年以上にわたる本番環境でのデバッグの知見が、フレームワークに組み込まれています
- ソケットに切り替えると、100以上のエッジケースに直面することになります
重要な違い: シミュレーターは macOS のネットワークスタックを使用し(iOS ではありません)、セルラー固有の問題(IPv6 のみのネットワークなど)が表面化せず、ネットワークの切り替わりもシミュレートしません。必須:実際のネットワーク環境で実機を使ってテストしてください。
必須の最初の手順
コードを変更する前に、必ず最初に以下のコマンドを実行してください。
// 1. Enable Network.framework logging
// Add to Xcode scheme: Product → Scheme → Edit Scheme → Arguments
// -NWLoggingEnabled 1
// -NWConnectionLoggingEnabled 1
// 2. Check connection state history
connection.stateUpdateHandler = { state in
print("\(Date()): Connection state: \(state)")
// Log every state transition with timestamp
}
// 3. Check TLS configuration
// If using custom TLS parameters:
print("TLS version: \(tlsParameters.minimumTLSProtocolVersion)")
print("Cipher suites: \(tlsParameters.tlsCipherSuites ?? [])")
// 4. Test with packet capture (Charles Proxy or Wireshark)
// On device: Settings → WiFi → (i) → Configure Proxy → Manual
// Charles: Help → SSL Proxying → Install Charles Root Certificate on iOS
// 5. Test on different networks
// - WiFi
// - Cellular (disable WiFi)
// - Airplane Mode → WiFi (test waiting state)
// - VPN active
// - IPv6-only (some cellular carriers)
ここからわかること
| 観察結果 | 診断 | 次の手順 |
|---|---|---|
| .preparing のまま5秒以上停止する | DNS 障害またはネットワーク停止 | パターン 1a |
| すぐに .waiting に移行する | 接続なし(機内モード、圏外) | パターン 1b |
| POSIX エラー 61 で .failed になる | 接続拒否(サーバーがリッスンしていない) | パターン 1c |
| POSIX エラー 50 で .failed になる | ネットワーク停止(インターフェースが無効) | パターン 1d |
| .ready の直後に .failed になる | TLS ハンドシェイクの失敗 | パターン 2b |
| .ready になり、送信は成功するが、データが届かない | フレーミングの問題、または受信側が処理していない | パターン 3a |
| WiFi では動作するが、セルラーでは失敗する | IPv6 のみのネットワーク(IPv4 のハードコード) | パターン 5a |
| VPN なしでは動作するが、VPN ありでは失敗する | プロキシによる干渉または DNS の上書き | パターン 5b |
必須の解釈
コードを変更する前に、以下のいずれかを特定してください。
- .preparing のまま停止し、ネットワークが利用可能 → DNS 障害(nslookup を確認)
- すぐに .waiting になり、機内モードがオフ → インターフェース固有の問題(セルラーがブロックされている)
- POSIX 61 で .failed → サーバーの問題(サーバーのログを確認)
- TLS エラー -9806 で .failed → 証明書の検証(openssl で確認)
- .ready だがデータが届かない → フレーミングまたは受信側の問題(パケットキャプチャを有効化)
診断結果に矛盾がある、または明確でない場合
- 停止してください。まだパターンに進まないでください
- すべての送受信呼び出しにタイムスタンプのログを追加してください
- パケットキャプチャを有効にしてください(Charles/Wireshark)
- 別のデバイスでテストし、ハードウェアとソフトウェアのどちらの問題か切り分けてください
決定木
これを使って、2分で適切な診断パターンにたどり着いてください。
Network problem?
├─ Connection never reaches .ready?
│ ├─ Stuck in .preparing for >5 seconds?
│ │ ├─ DNS lookup timing out? → Pattern 1a (DNS Failure)
│ │ ├─ Network available but can't reach host? → Pattern 1c (Connection Refused)
│ │ └─ First connection slow, subsequent fast? → Pattern 1e (DNS Caching)
│ │
│ ├─ Moves to .waiting immediately?
│ │ ├─ Airplane Mode or no signal? → Pattern 1b (No Connectivity)
│ │ ├─ Cellular blocked by parameters? → Pattern 1b (Interface Restrictions)
│ │ └─ VPN connecting? → Wait and retry
│ │
│ ├─ .failed with POSIX error 61?
│ │ └─ → Pattern 1c (Connection Refused)
│ │
│ └─ .failed with POSIX error 50?
│ └─ → Pattern 1d (Network Down)
│
├─ Connection reaches .ready, then fails?
│ ├─ Fails immediately after .ready?
│ │ ├─ TLS error -9806? → Pattern 2b (Certificate Validation)
│ │ ├─ TLS error -9801? → Pattern 2b (Protocol Version)
│ │ └─ POSIX error 54? → Pattern 2d (Connection Reset)
│ │
│ ├─ Fails after network change (WiFi → cellular)?
│ │ ├─ No viabilityUpdateHandler? → Pattern 2a (Viability Not Handled)
│ │ ├─ Didn't detect better path? → Pattern 2a (Better Path)
│ │ └─ IPv6 → IPv4 transition? → Pattern 5a (Dual Stack)
│ │
│ ├─ Fails after timeout?
│ │ └─ → Pattern 2c (Receiver Not Responding)
│ │
│ └─ Random disconnects?
│ └─ → Pattern 2d (Network Instability)
│
├─ Data not arriving?
│ ├─ Send succeeds, receive never returns?
│ │ ├─ No message framing? → Pattern 3a (Framing Problem)
│ │ ├─ Wrong byte count? → Pattern 3b (Min/Max Bytes)
│ │ └─ Receiver not calling receive()? → Check receiver code
│ │
│ ├─ Partial data arrives?
│ │ ├─ receive(exactly:) too large? → Pattern 3b (Chunking)
│ │ ├─ Sender closing too early? → Check sender lifecycle
│ │ └─ Buffer overflow? → Pattern 3b (Buffer Management)
│ │
│ ├─ Data corrupted?
│ │ ├─ TLS disabled? → Pattern 3c (No Encryption)
│ │ ├─ Binary vs text encoding? → Check ContentType
│ │ └─ Byte order (endianness)? → Use network byte order
│ │
│ └─ Works sometimes, fails intermittently?
│ └─ → Pattern 3d (Race Condition)
│
├─ Performance degrading?
│ ├─ Latency increasing over time?
│ │ ├─ TCP congestion? → Pattern 4a (Congestion Control)
│ │ ├─ No contentProcessed pacing? → Pattern 4a (Buffering)
│ │ └─ Server overloaded? → Check server metrics
│ │
│ ├─ Throughput decreasing?
│ │ ├─ Network transition WiFi → cellular? → Pattern 4b (Bandwidth Change)
│ │ ├─ Packet loss increasing? → Pattern 4b (Network Quality)
│ │ └─ Multiple streams competing? → Pattern 4b (Prioritization)
│ │
│ ├─ High CPU usage?
│ │ ├─ Not using batch for UDP? → Pattern 4c (Batching)
│ │ ├─ Too many small sends? → Pattern 4c (Coalescing)
│ │ └─ Using sockets instead of Network.framework? → Migrate (30% CPU savings)
│ │
│ └─ Memory growing?
│ ├─ Not releasing connections? → Pattern 4d (Connection Leaks)
│ ├─ Not cancelling on deinit? → Pattern 4d (Lifecycle)
│ └─ Missing [weak self]? → Pattern 4d (Retain Cycles)
│
└─ Works on WiFi, fails on cellular/VPN?
├─ IPv6-only cellular network?
│ ├─ Hardcoded IPv4 address? → Pattern 5a (IPv4 Literal)
│ ├─ getaddrinfo with AF_INET only? → Pattern 5a (Address Family)
│ └─ Works on some carriers, not others? → Pattern 5a (Regional IPv6)
│
├─ Corporate VPN active?
│ ├─ Proxy configuration failing? → Pattern 5b (PAC)
│ ├─ DNS override blocking hostname? → Pattern 5b (DNS)
│ └─ Certificate pinning failing? → Pattern 5b (TLS in VPN)
│
├─ Port blocked by firewall?
│ ├─ Non-standard port? → Pattern 5c (Firewall)
│ ├─ Outbound only? → Pattern 5c (NATing)
│ └─ Works on port 443, not 8080? → Pattern 5c (Port Scanning)
│
├─ Peer-to-peer connection failing?
│ ├─ NAT traversal issue? → Pattern 5d (STUN/TURN)
│ ├─ Symmetric NAT? → Pattern 5d (NAT Type)
│ └─ Local network only? → Pattern 5d (Bonjour/mDNS)
│
└─ URLSession fails but NWConnection works?
├─ HTTP URL blocked? → Pattern 6a (ATS HTTP Block)
├─ "SSL error" on HTTPS? → Pattern 6b (ATS TLS Version)
└─ Works on older iOS? → Pattern 6a/6b (ATS enforcement)
パターン選択ルール(必須)
パターンに進む前に、次のルールを確認してください。
- 接続が .ready に到達しない → パターン 1(DNS、接続性、拒否)から開始します
- TLS エラーコードが発生する → パターン 2b(証明書の検証)に直接進みます
- データが届かない → まずパケットキャプチャを有効にしてから、パターン 3 に進みます
- 特定のネットワークでのみ発生する(Wi-Fi では動作し、セルラーでは失敗する) → そのネットワークでテストし、パターン 5 に進みます
- パフォーマンスが低下する → Instruments の Network テンプレートでプロファイリングし、パターン 4 に進みます
パターンは一度に 1 つだけ適用する
- 1 つのパターンの修正を実装します
- 十分にテストします
- 問題が続く場合にのみ、次のパターンを試します
- 複数のパターンを同時に適用してはいけません(原因を特定できなくなります)
禁止事項
- 診断せずに解決策を推測すること
- 一度に複数の変更を行うこと
- 「タイムアウトを延ばせばよい」と決めつけること
- 「一時的に」TLS を無効にすること
- 「フレームワークの問題を避ける」ためにソケットへ切り替えること
診断パターン
パターン 1a:DNS 解決の失敗
所要時間:10~15 分
症状
- 接続が .preparing のまま 5 秒以上停止する
- 最終的に失敗するか、タイムアウトする
- IP アドレスでは動作するが、ホスト名では動作しない
- あるネットワークでは動作するが、別のネットワークでは失敗する
診断
// Enable DNS logging
// -NWLoggingEnabled 1
// Check DNS resolution manually
// Terminal: nslookup example.com
// Terminal: dig example.com
// Logs show:
// "DNS lookup timed out"
// "getaddrinfo failed: 8 (nodename nor servname provided)"
よくある原因
- DNS サーバーに到達できない(企業ネットワークが外部 DNS をブロックしている)
- ホスト名の入力ミス、またはホスト名が存在しない
- DNS キャッシュに古いエントリーが残っている(まれですが、発生します)
- VPN が DNS 解決をブロックしている
修正
// ❌ WRONG — Adding timeout doesn't fix DNS
/*
let parameters = NWParameters.tls
parameters.expiredDNSBehavior = .allow // Doesn't help if DNS never resolves
*/
// ✅ CORRECT — Verify hostname, test DNS manually
// 1. Test DNS manually:
// $ nslookup your-hostname.com
// If this fails, DNS is the problem (not your code)
// 2. If DNS works manually but not in app:
// Check if VPN or enterprise config blocking app DNS
// 3. If hostname doesn't exist:
let connection = NWConnection(
host: NWEndpoint.Host("correct-hostname.com"), // Fix typo
port: 443,
using: .tls
)
// 4. If DNS caching issue (rare):
// Restart device to clear DNS cache
// Or use IP address temporarily while investigating DNS server issue
検証
nslookup your-hostname.comを実行します。1 秒未満で IP アドレスが返されるはずです- セルラー(異なる DNS サーバー)でテストします。動作するはずです
- 企業ネットワークの DNS 設定を確認します
予防
- 広く知られているホスト名を使用します(内部 DNS に依存しないでください)
- 開発中に複数のネットワークでテストします
- IP アドレスをハードコードしないでください(DNS が失敗する場合は、回避するのではなく DNS を修正する必要があります)
パターン 2b:TLS 証明書の検証失敗
所要時間:15~20 分
症状
- 接続が一時的に .ready に到達した後、すぐに .failed になる
- エラー:
-9806(kSSLPeerCertInvalid) - エラー:
-9807(kSSLPeerCertExpired) - エラー:
-9801(kSSLProtocol) - 一部のサーバーでは動作するが、ほかのサーバーでは失敗する
診断
# Test TLS manually with openssl
openssl s_client -connect example.com:443 -showcerts
# Check certificate details
openssl s_client -connect example.com:443 | openssl x509 -noout -dates
# notBefore: Jan 1 00:00:00 2024 GMT
# notAfter: Dec 31 23:59:59 2024 GMT ← Check if expired
# Check certificate chain
openssl s_client -connect example.com:443 -showcerts | grep "CN="
# Should show: Subject CN=example.com, Issuer CN=Trusted CA
よくある原因
- 自己署名証明書(開発/ステージングサーバー)
- 証明書の有効期限切れ
- 証明書のホスト名が一致しない(証明書は「example.com」用だが、「www.example.com」に接続している)
- 中間 CA 証明書が不足している
- TLS 1.0/1.1(iOS 13 以降では TLS 1.2 以降が必要)
修正
無効な証明書を使用している本番サーバーの場合
// ❌ WRONG — Never disable certificate validation in production
/*
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { ... }, .main)
// This disables validation → security vulnerability
*/
// ✅ CORRECT — Fix the certificate on server
// 1. Renew expired certificate (Let's Encrypt, DigiCert, etc.)
// 2. Ensure hostname matches (CN=example.com or SAN includes example.com)
// 3. Include intermediate CA certificates on server
// 4. Test with: openssl s_client -connect example.com:443
開発サーバーの場合(一時的)
// ⚠️ ONLY for development/staging
#if DEBUG
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(
tlsOptions.securityProtocolOptions,
{ (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
// Trust any certificate (DEV ONLY)
sec_protocol_verify_complete(true)
},
.main
)
let parameters = NWParameters(tls: tlsOptions)
let connection = NWConnection(host: "dev-server.example.com", port: 443, using: parameters)
#endif
証明書のピンニングの場合
// Production-grade certificate pinning
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(
tlsOptions.securityProtocolOptions,
{ (metadata, trust, complete) in
let trust = sec_protocol_metadata_copy_peer_public_key(metadata)
// Compare trust with pinned certificate
let pinnedCertificateData = Data(/* your cert */)
let serverCertificateData = SecCertificateCopyData(trust) as Data
if serverCertificateData == pinnedCertificateData {
complete(true)
} else {
complete(false) // Reject non-pinned certificates
}
},
.main
)
検証
openssl s_client -connect example.com:443の結果にVerify return code: 0 (ok)と表示される- 証明書の有効期限が 30 日以上先である
- 証明書の CN がホスト名と一致する
- 実機の iOS デバイスでテストする(シミュレーターだけでテストしない)
パターン 3a:メッセージのフレーミング問題
所要時間:20~30 分
症状
- connection.send() はエラーなく成功する
- connection.receive() がデータを返さない
- または、receive() が部分的なデータを返す
- パケットキャプチャではネットワーク上のバイト列を確認できるが、アプリが処理しない
診断
// Enable detailed logging
connection.send(content: data, completion: .contentProcessed { error in
if let error = error {
print("Send error: \(error)")
} else {
print("✅ Sent \(data.count) bytes at \(Date())")
}
})
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, context, isComplete, error in
if let error = error {
print("Receive error: \(error)")
} else if let data = data {
print("✅ Received \(data.count) bytes at \(Date())")
}
}
// Use Charles Proxy or Wireshark to verify bytes on wire
よくある原因:ストリームプロトコル(TCP/TLS)はメッセージ境界を保持しません。
例
// Sender sends 3 messages:
send("Hello") // 5 bytes
send("World") // 5 bytes
send("!") // 1 byte
// Receiver might get:
receive() → "HelloWorld!" // All 11 bytes at once
// Or:
receive() → "Hel" // 3 bytes
receive() → "loWorld!" // 8 bytes
// Message boundaries lost!
修正
解決策 1:TLV フレーミングを使用する(iOS 26 以降)
// NetworkConnection with TLV
let connection = NetworkConnection(
to: .hostPort(host: "example.com", port: 1029)
) {
TLV {
TLS()
}
}
// Send typed messages
enum MessageType: Int {
case chat = 1
case ping = 2
}
let chatData = Data("Hello".utf8)
try await connection.send(chatData, type: MessageType.chat.rawValue)
// Receive typed messages
let (data, metadata) = try await connection.receive()
if metadata.type == MessageType.chat.rawValue {
print("Chat message: \(String(data: data, encoding: .utf8)!)")
}
解決策 2:手動で長さのプレフィックスを付ける(iOS 12~25)
// Sender: Prefix message with UInt32 length
func sendMessage(_ message: Data) {
var length = UInt32(message.count).bigEndian
let lengthData = Data(bytes: &length, count: 4)
connection.send(content: lengthData, completion: .contentProcessed { _ in
connection.send(content: message, completion: .contentProcessed { _ in
print("Sent message with length prefix")
})
})
}
// Receiver: Read length, then read message
func receiveMessage() {
// 1. Read 4-byte length
connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { lengthData, _, _, error in
guard let lengthData = lengthData else { return }
let length = lengthData.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
// 2. Read message of exact length
connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { messageData, _, _, error in
guard let messageData = messageData else { return }
print("Received complete message: \(messageData.count) bytes")
}
}
}
検証
- 10 個のメッセージを送信し、受信側がちょうど 10 個のメッセージを受け取ることを確認します
- サイズの異なるメッセージ(1 バイト、1000 バイト、64KB)を送信します
- パケット損失をシミュレーションしてテストします(Network Link Conditioner)
パターン 4a:TCP の輻輳とバッファリング
所要時間:15~25 分
症状
- 最初の数回の送信は速いが、その後、次第に遅くなる
- レイテンシが時間の経過とともに 50ms → 500ms → 2000ms と増加する
- メモリ使用量が増加する(未送信データがバッファリングされている)
- ユーザーから、5 分後にアプリの「動作が重く感じられる」と報告される
診断
// Monitor send completion time
let sendStart = Date()
connection.send(content: data, completion: .contentProcessed { error in
let elapsed = Date().timeIntervalSince(sendStart)
print("Send completed in \(elapsed)s") // Should be < 0.1s normally
// If > 1s, TCP congestion or receiver not draining fast enough
})
// Profile with Instruments
// Xcode → Product → Profile → Network template
// Check "Bytes Sent" vs "Time" graph
// Should be smooth line, not stepped/stalled
よくある原因
- 送信側が受信側で処理できる速度を超えて送信している(バックプレッシャー)
- ネットワークの混雑(パケット損失、再送)
- contentProcessed コールバックによるペーシングがない
- 接続が使用不能になった後も、その接続で送信している
修正
// ❌ WRONG — Sending without pacing
/*
for frame in videoFrames {
connection.send(content: frame, completion: .contentProcessed { _ in })
// Buffers all frames immediately → memory spike → congestion
}
*/
// ✅ CORRECT — Pace with contentProcessed callback
func sendFrameWithPacing() {
guard let nextFrame = getNextFrame() else { return }
connection.send(content: nextFrame, completion: .contentProcessed { [weak self] error in
if let error = error {
print("Send error: \(error)")
return
}
// contentProcessed = network stack consumed frame
// NOW send next frame (pacing)
self?.sendFrameWithPacing()
})
}
// Start pacing
sendFrameWithPacing()
代替案:Async/await(iOS 26+)
// NetworkConnection with natural back pressure
func sendFrames() async throws {
for frame in videoFrames {
try await connection.send(frame)
// Suspends automatically if network can't keep up
// Built-in back pressure, no manual pacing needed
}
}
検証
- 1000 件のメッセージを送信し、メモリ使用量を監視します(一定に保たれるはずです)
- 送信完了までの時間を監視します(100ms 未満に保たれるはずです)
- Network Link Conditioner を使ってテストします(遅延 100ms、パケット損失 3%)
パターン 5a:IPv6 専用のセルラーネットワーク(IPv4 のハードコード)
所要時間 10~15 分
症状
- WiFi(IPv4/IPv6 デュアルスタック)では問題なく動作する
- セルラー(IPv6 専用)では必ず失敗する
- 一部の通信事業者(T-Mobile)では動作するが、別の通信事業者(Verizon)では失敗する
- ログに「Host unreachable」または POSIX エラー 65(EHOSTUNREACH)が表示される
診断
# Check if hostname has IPv6
dig AAAA example.com
# Check if device is on IPv6-only network
# Settings → WiFi/Cellular → (i) → IP Address
# If starts with "2001:" or "fe80:" → IPv6
# If "192.168" or "10." → IPv4
# Test with IPv6-only simulator
# Xcode → Devices → (device) → Use as Development Target
# Settings → Developer → Networking → DNS64/NAT64
よくある原因
- IPv4 アドレスをハードコードしている(「192.168.1.1」)
- AF_INET のみを指定して getaddrinfo を呼び出している(IPv6 が除外される)
- サーバーに IPv6 アドレス(AAAA レコード)がない
- Connect by Name を使わず、手動で DNS を指定している
修正
// ❌ WRONG — Hardcoded IPv4
/*
let host = "192.168.1.100" // Fails on IPv6-only cellular
*/
// ❌ WRONG — Forcing IPv4
/*
let parameters = NWParameters.tcp
parameters.requiredInterfaceType = .wifi
parameters.ipOptions.version = .v4 // Fails on IPv6-only
*/
// ✅ CORRECT — Use hostname, let framework handle IPv4/IPv6
let connection = NWConnection(
host: NWEndpoint.Host("example.com"), // Hostname, not IP
port: 443,
using: .tls
)
// Framework automatically:
// 1. Resolves both A (IPv4) and AAAA (IPv6) records
// 2. Tries IPv6 first (if available)
// 3. Falls back to IPv4 (Happy Eyeballs)
// 4. Works on any network (IPv4, IPv6, dual-stack)
検証
- セルラー接続の実機でテストします(WiFi を無効にします)
- 複数の通信事業者(Verizon、AT&T、T-Mobile)でテストします
- 開発者設定で DNS64/NAT64 を有効にします
dig AAAA your-hostname.comを実行して、IPv6 レコードが存在することを確認します
本番環境での危機的状況
状況:iOS のアップデートにより接続障害が 15% 発生
状況
- 月曜日の朝、会社が iOS アプリのアップデート(v4.2)をリリースします
- 正午までに、カスタマーサポートから「アプリが動作しない」という問い合わせが急増したとの報告があります
- 分析結果では、ユーザーの 15%(10,000 人以上)で接続障害が発生しています
- CEO から Slack に「何が起きている? どれくらい早く修正できる?」とメッセージが届きます
- エンジニアリングマネージャーから修正予定時刻を尋ねられます
- あなたはネットワークエンジニアです
プレッシャーの兆候
- 🚨 本番環境の障害 10K 人以上に影響、収益への打撃、App Store に低評価レビューが投稿され始める
- ⏰ 時間的なプレッシャー 「至急修正が必要。Twitter で話題になっている」
- 👔 経営陣の注目 CEO 自ら進捗を確認している
- 📊 公開イメージ App Store の評価が 3 時間で 4.8 から 4.1 に低下
- 💸 金銭的な影響 E コマースアプリでは、売上の損失が 1 分あたり $5K に上る
誤った正当化(これらに陥らないでください)
-
「v4.1 にロールバックすればいい」
- その場しのぎとしては魅力的ですが、アプリの審査に 1~2 時間、ユーザーがアップデートするまでにさらに 24 時間かかります
- 根本原因が見つからず、再発する可能性があります
- 数週間かけて開発した v4.2 の機能が失われます
-
「原因を絞り込むために、TLS を一時的に無効にしよう」
- セキュリティ上の脆弱性が生じ、App Store で却下されます
- 実際の問題は解決せず、症状を隠すだけです
- いつ再び有効にしますか?(ヒント:「後で」修正しようとしても、結局実行されないため、永遠に再有効化されません)
-
「自分のデバイスでは動くから、ユーザー側の問題に違いない」
- これは診断ではなく、傲慢さです
- 10K 人のユーザーに同じ「エラー」が起きているのに、ユーザー側の問題でしょうか?
-
「リトライ処理とタイムアウトを追加しよう」
- 根本原因には対処できません
- 問題を悪化させます(リトライが増えるほど、障害が起きている経路への負荷も増えます)
必須の診断手順
CEO に以下を報告するまで、あなたには 1 時間あります。
- 根本原因
- 修正の所要時間
- 緩和策
ステップ 1:ベースラインを確認する(5 分)
// Check what changed in v4.2
git diff v4.1 v4.2 -- NetworkClient.swift
// Most likely culprits:
// - TLS configuration changed
// - Added certificate pinning
// - Changed connection parameters
// - Updated hostname
ステップ 2:本番環境で再現する(10 分)
// Check failure pattern:
// - Random 15%? Or specific user segment?
// - Specific iOS version? (check analytics)
// - Specific network? (WiFi vs cellular)
// Enable logging on production builds (emergency flag):
#if PRODUCTION
if UserDefaults.standard.bool(forKey: "EnableNetworkLogging") {
// -NWLoggingEnabled 1
}
#endif
// Ask Customer Support to enable for affected users
// Check logs for specific error code
ステップ 3:最近のコード変更を確認する(5 分)
// Found in git diff:
// v4.1:
let parameters = NWParameters.tls
// v4.2:
let tlsOptions = NWProtocolTLS.Options()
tlsOptions.minimumTLSProtocolVersion = .TLSv13 // ← SMOKING GUN
let parameters = NWParameters(tls: tlsOptions)
特定された根本原因 一部のユーザーが利用するバックエンドインフラ(ロードバランサー、プロキシサーバー)が TLS 1.3 に対応していません。v4.1 では TLS 1.2 がネゴシエートされていましたが、v4.2 では TLS 1.3 が必須となり、接続に失敗します。
ステップ 4:対象を絞った修正を適用する(15 分)
// Fix: Support both TLS 1.2 and TLS 1.3
let tlsOptions = NWProtocolTLS.Options()
tlsOptions.minimumTLSProtocolVersion = .TLSv12 // ✅ Support older infrastructure
// TLS 1.3 will still be used where supported (automatic negotiation)
let parameters = NWParameters(tls: tlsOptions)
ステップ 5:ホットフィックスをデプロイする(20 分)
# Build hotfix v4.2.1
# Test on affected user's network (critical!)
# Submit to App Store with expedited review request
# Explain: "Production outage affecting 15% of users"
プロフェッショナルな連絡用テンプレート
CEO 宛て(危機発生から 15 分後)
Found root cause: v4.2 requires TLS 1.3, but 15% of users on older infrastructure
(enterprise proxies, older load balancers) that only support TLS 1.2.
Fix: Change minimum TLS version to 1.2 (backward compatible, 1.3 still used when available).
ETA: Hotfix v4.2.1 in App Store in 1 hour (expedited review).
Full rollout to users: 24 hours.
Mitigation now: Telling affected users to update immediately when available.
エンジニアリングマネージャー宛て
Root cause: TLS version requirement changed in v4.2 (TLS 1.3 only).
15% of users behind infrastructure that doesn't support TLS 1.3.
Technical fix: Set tlsOptions.minimumTLSProtocolVersion = .TLSv12
This allows backward compatibility while still using TLS 1.3 where supported.
Testing: Verified fix on user's network (enterprise VPN with old proxy).
Deployment: Hotfix build in progress, ETA 30 minutes to submit.
Prevention: Add TLS compatibility testing to pre-release checklist.
カスタマーサポート宛て
Update: We've identified the issue and have a fix deploying within 1 hour.
Affected users: Those on enterprise networks or older ISP infrastructure.
Workaround: None (network level issue).
Expected resolution: v4.2.1 will be available in App Store in 1 hour.
Ask users to update immediately.
Updates: I'll notify you every 30 minutes.
節約できる時間
| アプローチ | 解決までの時間 | ユーザーへの影響 |
|---|---|---|
| ❌ パニックによるロールバック | 1〜2 時間のアプリレビュー + 24 時間のユーザー更新 = 26 時間 | 10K ユーザーが 26 時間ダウン |
| ❌ 「リトライを増やす」 | 不明 (根本原因を解決しない) | 15% の失敗率が永続化 |
| ❌ 「自分の環境では動く」 | 見当違いのデバッグに数日 | ユーザーの不満、悪いレビュー |
| ✅ 体系的な診断 | 30 分の診断 + 20 分の修正 + 1 時間のレビュー = 2 時間 | 10K ユーザーが 2 時間ダウン |
得られた教訓
- 多様なネットワークでテストする WiFi だけでテストしないでください。セルラー、VPN、企業ネットワークでテストしましょう。
- TLS 互換性を監視する TLS 設定を変更する場合は、バックエンドがそれをサポートしていることを確認してください。
- 段階的なロールアウト 段階的ロールアウト (10% → 50% → 100%) を使用して、問題を早期に発見しましょう。
- 緊急時のログ 本番環境で詳細ログを有効にして診断できる手段を用意しておきましょう。
- コミュニケーションの頻度 30 分ごとにステークホルダーに更新を伝えましょう。「まだ調査中です」だけでも構いません。
クイックリファレンス表
| 症状 | 考えられる原因 | 最初に確認すること | パターン | 修正時間 |
|---|---|---|---|---|
| .preparing でスタック | DNS 障害 | nslookup hostname | 1a | 10〜15 分 |
| すぐに .waiting | 接続性なし | 機内モード? | 1b | 5 分 |
| .failed POSIX 61 | 接続が拒否された | サーバーはリッスンしているか? | 1c | 5〜10 分 |
| .failed POSIX 50 | ネットワークダウン | インターフェースを確認 | 1d | 5 分 |
| TLS エラー -9806 | 証明書が無効 | openssl s_client | 2b | 15〜20 分 |
| データを受信しない | フレーミングの問題 | パケットキャプチャ | 3a | 20〜30 分 |
| 部分的なデータ | min/max バイトが誤っている | receive() のパラメータを確認 | 3b | 10 分 |
| レイテンシが増加 | TCP 輻輳 | contentProcessed のペーシング | 4a | 15〜25 分 |
| 高い CPU | バッチ処理なし | connection.batch を使用 | 4c | 10 分 |
| メモリが増加 | 接続リーク | [weak self] を確認 | 4d | 10〜15 分 |
| WiFi では動くがセルラーでは失敗 | IPv6 のみのネットワーク | dig AAAA hostname | 5a | 10〜15 分 |
| VPN なしでは動くが VPN ありでは失敗 | プロキシの干渉 | PAC ファイルをテスト | 5b | 20〜30 分 |
| ポートがブロックされる | ファイアウォール | 443 と 8080 を試す | 5c | 10 分 |
| HTTP URL がサイレントにブロックされる | ATS の適用 | Info.plist を確認 | 6a | 5〜10 分 |
| 「An SSL error has occurred」 | ATS の TLS 要件 | サーバーの TLS バージョンを確認 | 6b | 10〜15 分 |
パターン 6: App Transport Security (ATS) の失敗
所要時間 5〜15 分
ATS はデフォルトですべての接続に HTTPS を強制します (iOS 9 以降)。ATS の失敗はサイレントです — 接続は一般的なエラーで失敗し、コンソールに ATS 固有のメッセージは表示されません。
パターン 6a: ATS による HTTP のブロック
症状
- URLSession リクエストが
NSURLErrorSecureConnectionFailed(-1200) またはNSURLErrorAppTransportSecurityRequiresSecureConnection(-1022) で失敗する - Network.framework の接続は動作するが URLSession は動作しない
- 古い iOS バージョンでは動作するが、新しいバージョンでは失敗する
- 明確なエラーメッセージがない — ただ「接続に失敗しました」だけ
診断
# Check if ATS is blocking the connection
nscurl --ats-diagnostics https://yourserver.com
# Shows exactly which ATS policy the server fails
// In console, look for:
// "App Transport Security has blocked a cleartext HTTP (http://) resource load"
// This only appears if OS-level logging is enabled
修正 — 特定の HTTP ドメインを許可する (推奨)
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>api.legacy-server.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
NSAllowsArbitraryLoads は使用しないでください — ATS が完全に無効になります。App Store レビューがこれを検出し、リジェクトされる可能性があります。ドメイン固有の例外を使用してください。
パターン 6b: ATS の TLS バージョン要件
症状
- 有効な証明書があるにもかかわらず、HTTPS 接続が「SSL エラー」で失敗する
- サーバーが TLS 1.0 または 1.1 を使用している (ATS は TLS 1.2 以上を要求)
nscurl --ats-diagnosticsが TLS バージョンの失敗を示す
診断
# Check server's TLS version
openssl s_client -connect yourserver.com:443 -tls1_2
# If this fails but -tls1 succeeds → server doesn't support TLS 1.2
修正 — サーバーをアップグレードする (推奨) か例外を追加する
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>legacy-api.example.com</key>
<dict>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
</dict>
</dict>
</dict>
より良い修正: サーバーを TLS 1.2 以上にアップグレードしてください。TLS ダウングレードのための ATS 例外は、App Store レビューの厳しい審査を引き起こします。
ATS と Network.framework の違い
ATS は URLSession および WKWebView の接続に適用されます。Network.framework (NWConnection/NetworkConnection) は ATS の対象ではありません — tlsOptions を介して TLS 設定を直接処理します。同じサーバーに対して URLSession が失敗するが NWConnection が成功する場合、ATS が原因である可能性がほぼ確実です。
よくある間違い
間違い 1:デバッグ前にログを有効にしない
問題: フレームワークの内部状態を確認せずに、ネットワークの問題をデバッグしようとしています。
失敗する理由: 何が起きているかを推測するしかありません。ログには、状態遷移、エラーコード、タイミングが正確に記録されます。
解決策
// Add to Xcode scheme BEFORE debugging:
// -NWLoggingEnabled 1
// -NWConnectionLoggingEnabled 1
// Or programmatically:
#if DEBUG
ProcessInfo.processInfo.environment["NW_LOGGING_ENABLED"] = "1"
#endif
間違い 2:WiFi でしかテストしない
問題: WiFi とモバイル通信では特性が異なります(IPv6 のみ、プロキシ設定、パケット損失)。
失敗する理由: 接続失敗の 40% はネットワーク固有のものです。WiFi でしかテストしないと、モバイル通信の問題を見逃します。
解決策
- WiFi をオフにして、実機でテストします
- 複数の通信事業者でテストします(Verizon、AT&T、T-Mobile では設定が異なります)
- VPN を有効にしてテストします(エンタープライズユーザー向け)
- Network Link Conditioner を使用します(Xcode → Devices)
間違い 3:POSIX エラーコードを無視する
問題: .failed(let error) が発生しても、ユーザーには汎用的な「接続に失敗しました」とだけ表示しています。
失敗する理由: エラーコードによって必要な対処は異なります。POSIX 61 はサーバーの問題、POSIX 50 はクライアントのネットワークの問題を示します。
解決策
if case .failed(let error) = state {
let posixError = (error as NSError).code
switch posixError {
case 61: // ECONNREFUSED
print("Server not listening, check server logs")
case 50: // ENETDOWN
print("Network interface down, check WiFi/cellular")
case 60: // ETIMEDOUT
print("Connection timeout, check firewall/DNS")
default:
print("Connection failed: \(error)")
}
}
間違い 4:状態遷移をテストしない
問題: 正常系(.preparing → .ready)しかテストしていません。.waiting、ネットワークの切り替え、失敗をテストしていません。
失敗する理由: 実際のユーザーは、ネットワークの切り替え(WiFi → モバイル通信)、機内モード、電波の弱い状態を経験します。
解決策
// Test with Network Link Conditioner:
// 1. 100% Loss — verify .waiting state shows "Waiting for network"
// 2. WiFi → None → WiFi — verify automatic reconnection
// 3. 3% packet loss — verify performance graceful degradation
間違い 5:シミュレーターと実機が同じだと思い込む
問題: シミュレーターでしかテストしていません。シミュレーターは iOS とは異なる macOS のネットワーク機能を使用し、モバイル通信にも対応していません。
失敗する理由: シミュレーターでは IPv6 のみの問題が表面化せず、ネットワークの切り替えも再現されず、DNS の動作も異なります。
解決策
- リリース前に、必ず実機でテストします
- 機内モードを切り替えてテストします(ネットワークの切り替えをシミュレート)
- モバイル通信のみでテストします(WiFi を無効にする)
関連項目
予防的なパターンについて
networking skill — アンチパターンを防ぐための規律:
- 危険信号:SCNetworkReachability、ブロッキングソケット、ハードコードされた IP アドレス
- パターン 1a:TLS を使用する NetworkConnection(適切な実装)
- パターン 2a:適切な状態処理を行う NWConnection
- プレッシャーのかかる状況:手抜きをせずに納期のプレッシャーに対処する方法
API リファレンスについて
network-framework-ref skill — API ドキュメント一式:
- NetworkConnection(iOS 26 以降):WWDC 2025 の全 12 例
- NWConnection(iOS 12~25):例を含む API 一式
- TLV フレーミング、Coder プロトコル、NetworkListener、NetworkBrowser
- ソケット、URLSession、NWConnection からの移行戦略
関連する問題について
swift-concurrency skill — async/await を使用する場合:
- パターン 3:Task クロージャー内の weak self(同様のメモリーリーク防止)
- 接続状態の更新における @MainActor の使用
- 接続失敗時の Task のキャンセル
最終更新日 2025-12-02 ステータス WWDC 2018/2025 に基づく、本番環境で実用可能な診断方法 テスト済み 実際の本番環境の問題に照らして、診断パターンを検証済み