---
title: iOSDC2026 標準SDKだけで作る デスクトップアプリ内HTTPサーバー
tags:  #ios #iosdc2026 #iosdc  
author: [Norihiko Oba](https://www.docswell.com/user/higan96)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/2JVVQN9PJQ.jpg?width=480
description: iOSDC2026で発表した標準SDKだけで作る デスクトップアプリ内HTTPサーバーについて
published: September 16, 26
canonical: https://www.docswell.com/s/higan96/ZX2D2M-iosdc2026
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/2JVVQN9PJQ.jpg)

標準SDKだけで作る
デスクトップアプリ内HTTPサーバー
NWListenerとPOSIX socketの落とし穴
Norihiko Oba / iOSDC Japan 2026 / Track D


# Page. 2

![Page Image](https://bcdn.docswell.com/page/5EGLWKZQJL.jpg)

大庭 規彦
@higan96
• iOSエンジニア
• 株式会社ユビレジ
• 個人開発
• Kujaku


# Page. 3

![Page Image](https://bcdn.docswell.com/page/4JQY3NLW7P.jpg)

Kujaku
生成画像を閲覧・分類するMacアプリ
HTTPサーバーとして動作し
LAN内で画像配信できる機能を開発
このときの開発でハマった「落とし穴」についてのお話になります


# Page. 4

![Page Image](https://bcdn.docswell.com/page/K74W1GD1E1.jpg)

本日話すこと
• サーバー実装の前準備
• 実装の開始と落とし穴
• 問題を切り分けて実験で絞り込む
• App Sandboxのログと設定から接続できる条件を探る


# Page. 5

![Page Image](https://bcdn.docswell.com/page/LJ1YGDZ5EG.jpg)

サーバー実装の前準備


# Page. 6

![Page Image](https://bcdn.docswell.com/page/GJWGKY9W72.jpg)

HTTPサーバー開発の3つの選択肢
1. Network.framework — NWListener（2018年〜）
• 2025年にはSwift Concurrency対応のNetworkListenerが追加
• Network framework is by far the best choice.（TN3151）
2. Darwin — POSIX socket
• import Darwinで呼べるBSD由来の標準API
3. それらを使うライブラリ（SwiftNIOなど、今回は扱わない）


# Page. 7

![Page Image](https://bcdn.docswell.com/page/4EZLZX9273.jpg)

HTTPサーバー開発の3つの選択肢
1. Network.framework — NWListener（2018年〜）
• 2025年にはSwift Concurrency対応のNetworkListenerが追加
• Network framework is by far the best choice.（TN3151）
2. Darwin — POSIX socket
• import Darwinで呼べるBSD由来の標準API
3. それらを使うライブラリ（SwiftNIOなど、今回は扱わない）


# Page. 8

![Page Image](https://bcdn.docswell.com/page/Y76WZ4D67V.jpg)

import Foundation;
import Network
let parameters: NWParametersBuilder&lt;TCP&gt; = .parameters { TCP() }.localPort(8080)
let listener = try NetworkListener(using: parameters)
try await listener.run { connection in
①portの確保 + ②接続の到着を監視
let request = try await connection.receive(atLeast: 1, atMost: 8192)
③dataの受信
let response = Data(
&quot;HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!&quot;.utf8
)
try await connection.send(response, endOfStream: true)
}
④応答して送信を終了


# Page. 9

![Page Image](https://bcdn.docswell.com/page/G75MWQ3574.jpg)

App Sandboxとは
• macOSがカーネルで強制するアクセス制御の仕組みで、アプリが乗っ取られ
るような場合の被害を制限する
• ネットワーク、ファイル、カメラなど、必要なアクセスをentitlementで宣言
する
• こちらを有効化するのがMac App Store配布には必須


# Page. 10

![Page Image](https://bcdn.docswell.com/page/9J29QPZGER.jpg)

App Sandboxの設定を行う


# Page. 11

![Page Image](https://bcdn.docswell.com/page/DEY4W5RGJM.jpg)

実装の開始と落とし穴


# Page. 12

![Page Image](https://bcdn.docswell.com/page/VJNY9ND878.jpg)

let queue = DispatchQueue(label: &quot;HTTPServer&quot;)
let listener = try NWListener(using: .tcp, on: 8080)
①portの準備
listener.stateUpdateHandler = { print(&quot;listener:&quot;, $0) }
②接続到着を監視する準備
listener.newConnectionHandler = { connection in
connection.stateUpdateHandler = { print(&quot;connection:&quot;, $0)}
connection.start(queue: queue)
③dataの受信
connection.receive(
minimumIncompleteLength: 1,
maximumLength: 8_192
) { _, _, _, _ in
let response = Data(&quot;HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!&quot;.utf8)
connection.send(content: response, completion: .contentProcessed { _ in connection.cancel() } )
}
}
④応答して
listener.start(queue: queue)
④接続を閉じる
①portの確保 + ②接続到着の監視を開始


# Page. 13

![Page Image](https://bcdn.docswell.com/page/YE9P2RGXJ3.jpg)

Mac Safari -&gt; 200 OK


# Page. 14

![Page Image](https://bcdn.docswell.com/page/GE8D5WV9ED.jpg)

iPhone Safari -&gt; 接続不可


# Page. 15

![Page Image](https://bcdn.docswell.com/page/LELMYN5M7R.jpg)

状況の整理
• Macのlocalhostでは ✅ 200 OK
• 同じWi-Fi（LAN）につながるiPhoneのブラウザからは接続不可
• エラーもなし
• NWListenerの各APIは「エラーを返していない」
• 待ち受け状態（NWListener.State）も.failed(error)にならない
• .ready（外部からの接続を待機中）のまま
• newConnectionHandlerが呼ばれない


# Page. 16

![Page Image](https://bcdn.docswell.com/page/4JMYNX36JW.jpg)

疑ったこと
端末間通信がルーターの
設定で禁止されている？
macOS irewallが
影響している？
Local Networkの権限が
許可されてない？
App Sandboxの受信権限を
f
設定し忘れ？
確認したこと
接続結果
未設定
不達のまま
ON -&gt; OFF
不達のまま
TCP受信は対象外
不達のまま
有効
不達のまま


# Page. 17

![Page Image](https://bcdn.docswell.com/page/PJR9DNQN79.jpg)

問題を切り分け、実験で絞り込む


# Page. 18

![Page Image](https://bcdn.docswell.com/page/PEXQ1N58JX.jpg)

アプリ内HTTPサーバーの通信の流れ
5つの層に切り分け、それぞれの層で検証する
1
2
3
4
5


# Page. 19

![Page Image](https://bcdn.docswell.com/page/3EK92N6LED.jpg)

実験1: localhostでの接続
■ 実験内容
Macのブラウザからlocalhost（127.0.0.1）へ接続
■ 検証内容
ブラウザ上に「Hello World!」と表示されHTTPが適切に処理され応答が
返ること


# Page. 20

![Page Image](https://bcdn.docswell.com/page/L73W4V6675.jpg)

localhost 200 OK


# Page. 21

![Page Image](https://bcdn.docswell.com/page/87DKQ8VMJG.jpg)

アプリ内HTTPサーバーの通信の流れ
少なくともHTTPは適切に処理されレスポンスが返っている
1
2
3
4
5


# Page. 22

![Page Image](https://bcdn.docswell.com/page/VJPKL8VQE8.jpg)

実験2: Python HTTPサーバーへのLAN内スマホからの接続
■ 実験内容
Mac上のPython HTTPサーバーにLAN内のスマホからアクセス
$ python3 -m http.server 8000 --bind 0.0.0.0
■ 検証内容
実行したディレクトリのファイルを見ることができた場合、LANを通りスマホ
からMacに到達したことが確認できる


# Page. 23

![Page Image](https://bcdn.docswell.com/page/2EVVQNZPEQ.jpg)

Python HTTPサーバーで繋がった


# Page. 24

![Page Image](https://bcdn.docswell.com/page/57GLWK4QEL.jpg)

アプリ内HTTPサーバーの通信の流れ
Pythonプロセスに到達したのでLAN 経路とiPhone側は問題ない
1
2
3
4
5


# Page. 25

![Page Image](https://bcdn.docswell.com/page/4EQY3NGWJP.jpg)

実験3: Darwin/POSIX socketでの接続
■ 実験内容
3つの選択肢として挙げたなかの2つ目
NWListenerより低レベルなBSD socket APIで同じHTTPサーバーを実装し、接
続がどこまで到達するかを観測する
■ 検証内容
socket、bind、listen、acceptのどこで止まるのかを確認する


# Page. 26

![Page Image](https://bcdn.docswell.com/page/KJ4W1G5171.jpg)

let fd = Darwin.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
guard fd &gt;= 0 else { throw .posix(&quot;socket&quot;, errno) }
var addr = sockaddr_in()
addr.sin_len = UInt8(MemoryLayout&lt;sockaddr_in&gt;.size)
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = UInt16(8080).bigEndian
addr.sin_addr = in_addr(s_addr: INADDR_ANY) // 0.0.0.0
guard Darwin.bind(fd, ...) == 0 else { throw .posix(&quot;bind&quot;, errno) }
guard Darwin.listen(fd, SOMAXCONN) == 0 else { throw .posix(&quot;listen&quot;, errno) }
let source = DispatchSource.makeReadSource( ileDescriptor: fd, queue: queue)
source.setEventHandler {
let client = Darwin.accept(fd, nil, nil)
guard client &gt;= 0 else {
print(&quot;accept:&quot;, String(cString: strerror(errno)))
return
}
// レスポンス送信部分は省略
f
}
self.source = source
source.resume()


# Page. 27

![Page Image](https://bcdn.docswell.com/page/LE1YGDW57G.jpg)

Macアプリ + Darwin/POSIX socketで繋がった


# Page. 28

![Page Image](https://bcdn.docswell.com/page/GEWGKY6WJ2.jpg)

アプリ内HTTPサーバーの通信の流れ
Darwin/POSIX socketで実装したアプリが「完全に」疎通した
1
2
3
4
5


# Page. 29

![Page Image](https://bcdn.docswell.com/page/47ZLZXY2J3.jpg)

NWListenerが原因なのか？
Darwin/POSIX socketで実装したアプリが疎通したことでAPIの違いに原因の可
能性があるのではないか？
■ 可能性
1. NWListenerに原因があり、APIの利用を諦めるべきなのか？
2. NWListenerとApp Sandboxなど「実行環境の組み合わせ」が原因なのか？
次の実験ではNWListenerをCLIで動かし、NWListenerが原因の線を消していく


# Page. 30

![Page Image](https://bcdn.docswell.com/page/YJ6WZ4R6JV.jpg)

実験4: NWListenerをCLIで実行
■ 実験内容
App SandboxなしのCLIで、同じNWListenerを動かす
■ 検証内容
アプリの外でiPhoneへのHTTP応答が届くか。
失敗した場合、断定はできないがAPIが関与している可能性が高まる。


# Page. 31

![Page Image](https://bcdn.docswell.com/page/GJ5MWQ85J4.jpg)

let queue = DispatchQueue(label: &quot;HTTPServer&quot;)
let listener = try NWListener(using: .tcp, on: 8080)
listener.stateUpdateHandler = { print(&quot;listener:&quot;, $0) }
listener.newConnectionHandler = { connection in
connection.stateUpdateHandler = { print(&quot;connection:&quot;, $0) }
connection.start(queue: queue)
connection.receive(minimumIncompleteLength: 1, maximumLength: 8_192) { _, _, _, _ in
let response = Data(&quot;HTTP/1.1 200 OK（省略）&quot;.utf8)
connection.send(content: response, completion: .contentProcessed { _ in
connection.cancel()
})
}
}
listener.start(queue: queue)
dispatchMain()


# Page. 32

![Page Image](https://bcdn.docswell.com/page/LE3W4V85E5.jpg)

NWListener + CLIで繋がった


# Page. 33

![Page Image](https://bcdn.docswell.com/page/8EDKQ8MY7G.jpg)

アプリ内HTTPサーバーの通信の流れ
NWListener CLIがすべての経路を通過した
1
2
3
4
5


# Page. 34

![Page Image](https://bcdn.docswell.com/page/V7PKL8R2J8.jpg)

アプリとCLIで何が違う？
アプリ
CLI
有効
なし
コード署名
Apple Development
ad-hoc
（開発者証明書なし）
アプリの識別情報
Bundle IDあり
なし
App Sandbox
App Sandboxを無効にした構成で、実験5を行う


# Page. 35

![Page Image](https://bcdn.docswell.com/page/2JVVQNRXJQ.jpg)

実験5: App Sandboxの無効化
■ 実験内容
NWListenerのコード・通信設定は変えずApp Sandboxを無効にして再ビルド
し、同じWi-Fiのスマホからブラウザでアクセスする
■ 検証内容
・App Sandboxの有効・無効で、接続の成否が変わるか


# Page. 36

![Page Image](https://bcdn.docswell.com/page/5EGLWKMRJL.jpg)

NWListener + アプリ + App Sandbox無効化で繋がった


# Page. 37

![Page Image](https://bcdn.docswell.com/page/4JQY3NRY7P.jpg)

実験5: App Sandboxの無効化
■ 結果
✅ App Sandboxを無効にするとHTTPレスポンスが届いた
再び有効にすると、接続できなくなった
■ わかったこと
App Sandboxの有無で接続の成否が分かれた
App Sandboxの関与は確認できたが、接続を拒否する内部の仕組みまでは特定
できていない


# Page. 38

![Page Image](https://bcdn.docswell.com/page/K74W1G8ZE1.jpg)

実験で確認したこと
② LAN
③ LAN→Mac
1. localhost
2. Pythonサーバー
3. Darwin
POSIX socket
未確認
LAN経由で到達
LAN経由で到達
未確認
Pythonプロセスに
到達
アプリに到達
④ LAN接続の受理
未確認
未確認
accept成功
⑤ HTTP処理
200 OK
未確認
200 OK
4. NWListener
+ CLI
5. App Sandbox
の無効化
LAN経由で到達
LAN経由で到達
CLIプロセスに
到達
アプリに到達
接続callbackが
接続callbackが
呼ばれる
呼ばれる
200 OK
200 OK


# Page. 39

![Page Image](https://bcdn.docswell.com/page/LJ1YGD2DEG.jpg)

App Sandboxのログと設定から
接続できる条件を探る


# Page. 40

![Page Image](https://bcdn.docswell.com/page/GJWGKYR872.jpg)

接続生成のログにEPERMの手がかり
iPhoneの接続時に記録されたログ（抜粋・改行）
nw_path_evaluator_create_ low_inner NECP_CLIENT_ACTION_ADD_FLOW …
[1: Operation not permitted]
nw_connection_create_from_protocol_on_nw_queue [C14] Failed to create
connection from listener
errno 1 = EPERM: 接続用 lowの追加が許可されていない
f
f
このログだけでは拒否の主体は分からないので、kernelログで確かめる


# Page. 41

![Page Image](https://bcdn.docswell.com/page/4EZLZXR973.jpg)

Sandboxのログにnetwork-outboundの拒否
Console.app で kernel のログを見る
14:37:09.727 (Sandbox) probe-server-only(26871) deny(1) network-outbound remote:*:51671
14:37:09.727 SK[0]: low_req_prepare
14:37:09.727 SK[0]: fsw_ low_add
low req failed MAC check
failed to add low_uuid … (err 1)
14:37:09.727 necp_client_add_ low: Add low error (1)
network-outboundの拒否とADD_FLOWのエラー1が同時刻・同一threadに記録
f
f
f
f
f
f
iPhoneからの受信接続の処理で、outboundが拒否されMAC（Mandatory
Access Control）checkが失敗していることが観測された


# Page. 42

![Page Image](https://bcdn.docswell.com/page/Y76WZ4RD7V.jpg)

Outgoing Connections (Client) を足す


# Page. 43

![Page Image](https://bcdn.docswell.com/page/G75MWQ8874.jpg)

Outgoing Connections (Client)の有効化で繋がった


# Page. 44

![Page Image](https://bcdn.docswell.com/page/9J29QPKVER.jpg)

繋がった
■ 検証結果（macOS 26.5.2）
✅ Incoming Connections + Outgoing ConnectionsでiPhoneへHTTP応答が届いた
✅ App Sandboxは有効のまま
■ 疑問
受信側の通信のはずなのになぜか送信側の通信として扱われApp Sandboxに拒否され
ている。
なんでこんな挙動なのか？


# Page. 45

![Page Image](https://bcdn.docswell.com/page/DEY4W5NQJM.jpg)

公開XNUから分かること
GitHubで公開されているXNU（kernel）の関連処理を要約すると、「相手のアドレスの有無」で
connect側／listen側のMAC hookを選んでいる(Sandbox に許可を問い合わせる処理)
// xnu: low_manager.c low_req_check_mac_allowed()の要約
if (!has_daddr &amp;&amp; !has_dport) // remote address も port もない
mac_skywalk_ low_check_listen(…) // listen側として check
else
mac_skywalk_ low_check_connect(…) // connect側として check
受信側として処理されるべき接続が送信側の処理に振られることでApp Sandboxの「Outgoing
f
f
f
f
Connections (Client)」の権限不足で拒否されたと考えるとログとも整合するので可能性が高い


# Page. 46

![Page Image](https://bcdn.docswell.com/page/VJNY9NX278.jpg)

まとめ
• 原因としてはkernelがアドレスを持つ受信側の通信を送信側（connect側）の
MAC hook（許可設定の確認）にまわしているのが原因の可能性が高い
• App Sandboxのpolicyが非公開なので断定はできない
• ここが落とし穴の正体と言えそう
• 通信を伴うMacアプリの場合、受信だけでも送信の許可をApp Sandboxで有効に
すると意図通り動くケースがある
• 環境に依存した振る舞いの可能性もあるので状況をみて設定する
• 問題を切り分けて実験を繰り返せばkernelの謎挙動までたどり着く


# Page. 47

![Page Image](https://bcdn.docswell.com/page/YE9P2R8DJ3.jpg)

実際に作ったもの
1. LAN閲覧機能を開始
2. QRコードの発行
3. QRコードを読み取りURLを取得したら
ブラウザでアクセス
4. 画像一覧が表示
5. 画像選択で詳細を表示


# Page. 48

![Page Image](https://bcdn.docswell.com/page/GE8D5W85ED.jpg)

ご清聴ありがとう
ございました


