この記事は、iOSアプリ開発から公開までの流れ の第12章です。
本稿では、Swift を使用したネットワークインターフェースのアドレス取得方法を記載します。
iPhone は複数のインターフェースを持つマルチホーム端末です。
以下のようなケースでは、端末のインターフェースアドレスを把握した上で Ping を実行する必要があります。
- マルチキャストアドレスを指定して送信する
- リミッテッドブロードキャストアドレス(255.255.255.255)を使用して送信する(あまりオススメできない)
- インターネット共有(Wi-Fi テザリング)を使用してプライベートネットワーク内にのみ通信する
- Wi-Fi に接続している状態であえてモバイル通信側からパケットを送信する
しかし、iPhone の 設定 ではインターフェースの詳細情報を確認できません。そこで、今回開発する Ping アプリに以下に示す ifconfig コマンド相当の機能を搭載してみます
% ifconfig -a inet
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
inet 127.0.0.1 netmask 0xff000000
:
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
options=400<CHANNEL_IO>
inet 10.74.51.178 netmask 0xffffe000 broadcast 10.74.63.255
:
##1. IPv4 アドレスの取得方法
getifaddrs を使用して IPv4 アドレスを持つインタフェースを対象にアドレスを確認してみます。
※今回は IPv6 は対象外とします。
import Darwin
import Foundation
let statusBits: [UInt32] = [0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000]
let statusLabels: [String] = ["UP", "BROADCAST", "DEBUG", "LOOPBACK", "POINTOPOINT", "SMART", "RUNNING", "NOARP", "PROMISC", "ALLMULTI", "OACTIVE", "SIMPLEX", "LINK0", "LINK1", "LINK2", "MULTICAST"]
func ifconfig_inet() {
var ifaPtr: UnsafeMutablePointer<ifaddrs>? = nil
var ifaListPtr: UnsafeMutablePointer<ifaddrs>? = nil
guard getifaddrs(&ifaListPtr) == 0 else {
return
}
defer {
freeifaddrs(ifaListPtr)
}
ifaPtr = ifaListPtr
while ifaPtr != nil {
guard let ifa = ifaPtr?.pointee else {
return
}
guard let ifaName = String(validatingUTF8: ifa.ifa_name) else {
return
}
if ifa.ifa_addr.pointee.sa_family == Int32(AF_INET) {
var output = "\(ifaName): " + String(format: "flags=%04x<", ifa.ifa_flags)
var flg = false
for i in 0 ..< statusBits.count {
if ifa.ifa_flags & statusBits[i] == statusBits[i] {
output += flg ? ",\(statusLabels[i])" : "\(statusLabels[i])"
flg = true
}
}
output += ">\n "
ifa.ifa_addr.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { sin in
output += " inet " + String.init(cString: inet_ntoa(sin.pointee.sin_addr))
}
ifa.ifa_netmask.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { sin in
output += " netmask " + String.init(cString: inet_ntoa(sin.pointee.sin_addr))
}
if ifa.ifa_flags & UInt32(IFF_BROADCAST) == UInt32(IFF_BROADCAST) {
ifa.ifa_dstaddr.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { sin in
output += " broadcast " + String.init(cString: inet_ntoa(sin.pointee.sin_addr))
}
}
print(output)
}
ifaPtr = ifa.ifa_next
}
}
ifconfig_inet()
Playground(macOS)上の実行結果を以下に示します。
期待通りの情報が確認できました。
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST>
inet 127.0.0.1 netmask 255.0.0.0
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST>
inet 10.xx:xx.16 netmask 255.255.224.0 broadcast 10.xx.xx.255
##2. MAC アドレス(Ethernet アドレス)の取得方法
iPhone は、設定 > 一般 > 情報 でネットワークインターフェースの MAC アドレス(Wi-Fi の方)を確認できます。
これを Swift 経由の C 言語で取得する方法を試してみます。
####2-1. getifaddrs(3) 関数
1.と同じ手法で、getifaddrs を使用して全リンク(L2 インタフェース)を対象にアドレスを確認してみます。
import Darwin
func ifconfig_link() {
var ifaPtr: UnsafeMutablePointer<ifaddrs>? = nil
var ifaListPtr: UnsafeMutablePointer<ifaddrs>? = nil
guard getifaddrs(&ifaListPtr) == 0 else {
return
}
defer {
freeifaddrs(ifaListPtr)
}
ifaPtr = ifaListPtr
while ifaPtr != nil {
guard let ifa = ifaPtr?.pointee else {
return
}
if ifa.ifa_addr.pointee.sa_family == Int32(AF_LINK) {
ifa.ifa_addr.withMemoryRebound(to: sockaddr_dl.self, capacity: 1) { sdl in
print(String(cString: link_ntoa(sdl)))
}
}
ifaPtr = ifa.ifa_next
}
}
ifconfig_link()
Playground(macOS)上の実行結果を以下に示します。
link_ntoa だと表示内容にインターフェース名が含まれ、かつ、アドレスがピリオド(.)区切りとなります。
(これだと見難いので、Ping アプリでは link_ntoa は使いません)
lo0
gif0
stf0
anpi0:1e.0.a2.1e.35.70
anpi1:1e.0.a2.1e.35.71
en3:1e.0.a2.1e.35.50
ap1:3a.3e.ef.c2.1.75
en4:1e.0.a2.1e.35.51
en0:18.3e.ef.c2.1.75
en1:36.61.f1.42.56.40
en2:36.61.f1.42.56.44
bridge0:36.61.f1.42.56.40
awdl0:de.ea.eb.8d.f8.6a
llw0:de.ea.eb.8d.f8.6a
utun0
utun1
utun2
utun3
####2-2. その他の方法について
Linux では、ioctl(2) システムコールでネットワークデバイスの参照用のマクロ SIOCGIFHWADDR で MAC アドレスを確認できますが、iOS にはそのような機能はないようです。
以下のように、sys/sockio.h にも該当する定義は見つかりませんでした。
SIOCSIFLLADDR がそれっぽいですが、SET だけが定義されていて GET がありません。
% grep "SIOC[G|S]IF.*ADDR" /System/Volumes/Data/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/sys/sockio.h
#define SIOCSIFADDR _IOW('i', 12, struct ifreq) /* set ifnet address */
#define SIOCSIFDSTADDR _IOW('i', 14, struct ifreq) /* set p-p address */
#define SIOCSIFBRDADDR _IOW('i', 19, struct ifreq) /* set broadcast addr */
#define SIOCGIFADDR _IOWR('i', 33, struct ifreq) /* get ifnet address */
#define SIOCGIFDSTADDR _IOWR('i', 34, struct ifreq) /* get p-p address */
#define SIOCGIFBRDADDR _IOWR('i', 35, struct ifreq) /* get broadcast addr */
#define SIOCSIFLLADDR _IOW('i', 60, struct ifreq) /* set link level addr */
#define SIOCSIFPHYADDR _IOW('i', 62, struct ifaliasreq) /* set gif addres */
#define SIOCGIFPSRCADDR _IOWR('i', 63, struct ifreq) /* get gif psrc addr */
#define SIOCGIFPDSTADDR _IOWR('i', 64, struct ifreq) /* get gif pdst addr */
また、setsockopt(2) で IP_RECVIF オプションを設定したソケットを使用し、recvmsg(2) でパケット受信時の制御メッセージで sockaddr_dl 構造体データを取り出してみました。
この場合、「設定 で確認できるアドレス」でも「02:00:00:00:00:00」でもない別の MAC で表示されました。
(機微な情報かもしれないため詳細は省略)
##3. iOS アプリの表示結果
getifaddrs() によるアドレス取得方法で確認できる情報を表示してみます。
各インターフェースの MTU やデフォルトルートも表示させたかったですが、root 権限なしで取得する方法がわからず断念。
MAC アドレスが 02:00:00:00:00:00 となっています。
最近のモバイル OS(Andoroid 含む)は、プライバシー保護のために MAC アドレスの取得を試みると実アドレスからダミーアドレスに置き換えるようなので、残念ですが仕方ないですね。
##4. まとめ・考察
アプリからアドレスを確認する場合は、getifaddrs() 関数 1 を使用すれば良さそう。
ただし、freeifaddrs() でちゃんとメモリを解放しないとメモリリークしてしまいますので注意が必要です。
iPhone では、アプリが使用できるネットワークインターフェースが少なくても 4 つ(以下の青丸)あります。
外部観測してみての推測となりますが、インターネット共有(テザリング)に接続すると en1 または en2(もしくは両方)のリンク上にブリッジを作成するようです。ブリッジに固定 IP (172.20.10.1) を割り当てて 172.20.10.0/28 の範囲で接続元に IP を払い出し、接続元からのパケットを IP マスカレードを経由してインターネットに転送していると思われます。
終わり。
-
Swift の標準ライブラリで getifaddrs 相当の情報を確認することができるよ!ということであれば教えてもらえると嬉しいです。 ↩