iOS端末ネットワークインターフェースのアドレス取得の実装と考察

> この記事は、[iOSアプリ開発から公開までの流れ](https://qiita.com/manabapp/private/6ae317682c5e161957ab) の第12章です。

本稿では、Swift を使用したネットワークインターフェースのアドレス取得方法を記載します。

iPhone は複数のインターフェースを持つマルチホーム端末です。
以下のようなケースでは、端末のインターフェースアドレスを把握した上で Ping を実行する必要があります。

- マルチキャストアドレスを指定して送信する
- リミッテッドブロードキャストアドレス（255.255.255.255）を使用して送信する（あまりオススメできない）
- インターネット共有（Wi-Fi テザリング）を使用してプライベートネットワーク内にのみ通信する
- Wi-Fi に接続している状態であえてモバイル通信側からパケットを送信する

しかし、iPhone の **設定** ではインターフェースの詳細情報を確認できません。そこで、今回開発する Ping アプリに以下に示す ifconfig コマンド相当の機能を搭載してみます

```Console:ifconfig&nbsp;コマンド結果（macOS&nbsp;上の結果）
% 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 は対象外とします。

```swift:getifaddrs&nbsp;を使用した&nbsp;IPv4&nbsp;アドレスの確認
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）上の実行結果を以下に示します。
期待通りの情報が確認できました。

```console:実行結果
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 の方）を確認できます。

<img alt="2.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/844861/a39def1d-401e-6fb8-641f-abe5986b6ce7.jpeg" width="300">

これを Swift 経由の C 言語で取得する方法を試してみます。

####2-1. getifaddrs(3) 関数

1.と同じ手法で、getifaddrs を使用して全リンク（L2 インタフェース）を対象にアドレスを確認してみます。

```swift:getifaddrs&nbsp;を使用した&nbsp;MAC&nbsp;アドレスの確認
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 は使いません）

```console:実行結果
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 がありません。

```console
% 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 権限なしで取得する方法がわからず断念。

<img alt="3.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/844861/02ae6e8a-1b66-dfbf-709b-4626f5027cce.jpeg" width="800">

MAC アドレスが 02:00:00:00:00:00 となっています。
最近のモバイル OS（Andoroid 含む）は、プライバシー保護のために MAC アドレスの取得を試みると実アドレスからダミーアドレスに置き換えるようなので、残念ですが仕方ないですね。

##4. まとめ・考察

アプリからアドレスを確認する場合は、getifaddrs() 関数 [^1] を使用すれば良さそう。
ただし、freeifaddrs() でちゃんとメモリを解放しないとメモリリークしてしまいますので注意が必要です。

[^1]: Swift の標準ライブラリで getifaddrs 相当の情報を確認することができるよ！ということであれば教えてもらえると嬉しいです。

iPhone では、アプリが使用できるネットワークインターフェースが少なくても 4 つ（以下の青丸）あります。
外部観測してみての推測となりますが、インターネット共有（テザリング）に接続すると en1 または en2（もしくは両方）のリンク上にブリッジを作成するようです。ブリッジに固定 IP (172.20.10.1) を割り当てて 172.20.10.0/28 の範囲で接続元に IP を払い出し、接続元からのパケットを IP マスカレードを経由してインターネットに転送していると思われます。

推測イメージはこんな感じ。
<img alt="4.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/844861/5a0a6191-4d87-e401-9159-ed8e4c4cff8e.jpeg" width="600">

終わり。