4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SwiftによるICMPソケット通信の実装と考察

Last updated at Posted at 2020-11-24

この記事は、iOSアプリ開発から公開までの流れ の第10章です。

本稿では、Swift で簡単な Ping を実際にプログラミングし、ICMP1 ソケット通信における実装方法のベストプラクティスについて考察してみます。

【最初に結論】
iOS での ICMP ソケット操作は POSIX Socket で実装するのがベター。
ただし、ある程度の C 言語スキルが必須。

CFSocket の実装については、CFSocket を使って iOS で socket 通信した話 の記事 (Qiita) を参考させてもらいました。m(_ _)m

1. ソケット API の種類

現在 iOS で使用可能なソケットは、以下に示す 3 種類があります。
今回の目的である Ping アプリでは ICMP プロトコルでソケット通信するため、POSIX Socket または CFSocket の API を使用する必要があります。

ソケット API TCP UDP ICMP
POSIX Socke
CFSocket
Network.framework
  • Network.framework の API には ICMP プロトコルの指定が存在しない模様。
  • CFSocket の API は、BSD ソケットで実装されていると記載あり。
  • URLSession もソケットに含まれるかもしれないが、前提となっている Network.framework が ICMP に対応していないためいずれにしても対象外。
  • RAW ソケットを使用して IP 以上のプロトコルヘッダ(ICMP を含む)を制御する手法もあるが、iOS では SOCK_RAW に必要な root 権限を持てない(脱獄すると未サポート扱いになる)ため対象外。

以上により、私が認識している各 API の相関関係は以下のとおり。
(間違ってたらご指摘ください)
1.jpeg

2. Ping の仕組み

一般的な Ping では、ICMP プロトコルのパケットを宛先ホストに送信し、応答有無によってホストの死活監視を行います。
また、通信経路上のゲートウェイ数(ホップ数)や応答時間なども併せて確認することができます。

応答が返らなかったり、途中のゲートウェイからエラーが返されたりもするため、Traceroute と併用してホスト or 通信経路上のゲートウェイの異常箇所切り分けにも利用されます。

  iOSデバイス     ゲートウェイ1      ゲートウェイ2    ...   ゲートウェイN        ホスト
      |               |               |                    |               |
      | Echo Request  |               |                    |               |
 Ping |-------------->|-------------->|---------...------->|-------------->|
      | (ICMP Type:8) |               |                    |               |
      |               |               |                    |  Echo Reply   |
      |<--------------|<--------------|<--------...--------|<--------------|
      |               |               |                    | (ICMP Type:0) |

  • Ping による送信パケット Echo(Type フィールド:8)と呼びます。
    ネットワーク I/F を持つ装置(仮想デバイスを含むルータやホスト)は Echo を受け取ると Echo reply(Type フィールド:0)を返します。この仕組みを利用して生存監視を実現しています。

  • UDP プロトコルによる Ping の実装もあります。
    ホストは未使用ポートに対するパケットを受け取ると、Port unreachable(Type フィールド:3 かつ Code フィールド:3)を返します。ポートが偶然使用中(この場合は応答有無はホスト上のサービスに依存)ということもあるため、宛先ポート番号をズラしながら複数の UDP パケットを送信することで、ホストの生存監視が可能となります。

  • Echo の代わりに、Address Mask Request(Type フィールド:17)等を使用したオプション実装もあります。ホストはこの要求を受け取ると、サブネットマスク情報を返却(Type フィールド:18)します。
    このように、ホスト OS にはパケットを受け取ると自動的に応答を返す機構がいくつか備わっており、Ping はこの仕組みを利用して生存を確認します。

3. 一般的な実装

macOS などの OS が標準で提供している一般的な Ping を簡単に実装してみます。
パケット送信数は1回のみ固定長・固定パラメタ値を使用したシンプルな実装とします。
比較のため、C 言語でも実装してみます。

  • 付録 A: C言語による実装例 (POSIX Socket)
  • 付録 B: Swift による実装例 (POSIX Socket)
  • 付録 C: Swift による実装例 (CFSocket)

処理の流れは以下のとおり。
詳細な説明は割愛します。各付録のソースコードを参照してください。

  1. ICMP ソケットを作成
  2. 宛先アドレスを作成
  3. 送信データグラムを作成
  4. Echo request を送信
  5. 応答を待ち合わせ
  6. Echo reply を受信

各実行結果は以下のとおり。
黄金ルートだけなら、いずれも OS 標準の Ping と同等の機能を実現できます。

C - POSIX Socket(macOS ターミナル)
% cc one_ping.c
% ./a.out      
PING ifconfig.io (104.24.122.146): 56 data bytes
64 bytes from 104.24.122.146: icmp_seq=0 ttl=54 time=12.272 ms
Swift - POSIX Socket(Xcode コンソール)
PING ifconfig.io (104.24.122.146): 56 data bytes
64 bytes from 104.24.122.146: icmp_seq=0 ttl=54 time=20.824 ms
Swift - CFSocket(Xcode コンソール)
PING ifconfig.io (104.24.122.146): 56 data bytes
64 bytes from 104.24.122.146: icmp_seq=0 ttl=54 time=32.037 ms

4. 実装方式の考察

Swift は、OS の関数プロトタイプ、定数、構造体などの定義情報を内包しており、PF_INET (定数) や struct sockaddr_in (構造体) を直接使用できることが特徴であり、C言語との親和性が高いと言われています。そのため、POSIX Socket や CFSocket などの C ベース API を使用して問題なく実装できます。
ただし、ICMP_ECHO (定数)や struct icmp (構造体) などは Xcode に取り込まれていないように見受けられ、付録 B,C のように自身で定義しなければならないようです。
このことから、Apple は iOS における ICMP ソケットのユーザー利用を想定していない、と推察されます。

(2020/12/18 追記)
ブリッジングヘッダーを使用することで定義を取り込めることがわかりました。以下のように記載することで各プロトコル(IP、ICMP、UDP、および TCP)の定義を参照できるようになりました。
ただ、struct icmp がうまく扱えません。icmp_id, icmp_seq メンバが union(共用体)で宣言されているためだと思われます。union を使用していな struct ipstruct udphdrICMP_ECHO などの define 値は使用できました。

SocPing-Bridging-Header.h(Ping アプリ用のブリッジングヘッダー)
#import <netinet/ip.h>
#import <netinet/ip_icmp.h>
#import <netinet/udp.h>
#import <netinet/tcp.h>

Using Sockets and Socket Streams (Appleサイト) には以下のように記載されています。
ここで言う non-TCP connections (非TCP接続) とは、UDP や ICMP を指しているはずです。

In iOS, POSIX networking is discouraged because it does not activate the cellular radio or on-demand VPN. Thus, as a general rule, you should separate the networking code from any common data processing functionality and rewrite the networking code using higher-level APIs.

(Google 訳)
iOSでは、POSIXネットワーキングはセルラー無線またはオンデマンドVPNをアクティブ化しないため、推奨されていません。したがって、原則として、ネットワークコードを一般的なデータ処理機能から分離し、高レベルのAPIを使用してネットワークコードを書き直す必要があります。

For daemons and services that listen on a port, or for non-TCP connections, use POSIX or Core Foundation (CFSocket) C networking APIs.

(Google 訳)
ポートでリッスンするデーモンとサービス、または非TCP接続の場合は、POSIXまたはCore Foundation(CFSocket)CネットワークAPIを使用します。

この記事は 2013年更新 ととても古いものですが、おそらく POSIX Socket や CFSocket などの low-level API は現在も推奨されていないと予想しています。
(もしかして、これから作成する Ping アプリはリジェクトされるのではと心配になってきました...)

現在では Network.framework によって「デーモンとサービス」「非 TCP 接続(UDP)」の機能が提供されていますが、依然として「非 TCP 接続(ICMP)」だけはこれらの low-level API を使用せざるを得ないことになるでしょう。

では、POSIX Socket と CFSocket のどちらを採用べきなのか、いくつかの観点で比較してみました。

Swift - POSIX Socket Swift - CFSocket
フレームワーク Darwin Core Foundation
プログラミングの手法 フロー駆動型
上から下に順番に呼び出す
イベント駆動型
コールバック関数を呼び出す
プログラミングの規模 163 Steps
(付録 B 参照)
159 Steps
(付録 C 参照)
プログラミングの特徴 ソケットオプションや制御メッセージなどの詳細制御が可能 コネクション制御やデータ送受信などの主要操作に特化した使いやすい操作性

フレームワークの違いによる性能差については、CFSocket の方は Core とあるだけにオーバーヘッドは気にする必要はない?こればかりは何とも言えません。
駆動方式については、UI との組み合わせと好みで選べば良さそうです。
コーディングの規模にも大きな違いはなさそうであり、慣れればどちらも問題なさそうです。

POSIX Socket については、iOS や macOS では BSD システムコールとして提供されていることから、直接カーネルに対してシステムコールを呼び出すことでソケット通信を実現できます。上述した定数・構造体等を自身で定義さえすれば、OS の機能を一般ユーザー権限の範囲でフルに利用できるはずです。
一方、CFSocket は、Apple ドキュメント を読む限り、ユーザーが使いやすいように BSD ソケット(POSIX Socket)をラッピングしたものと想定されますが、代償として機能が制限されていると感じます。

(結論)
今回のように簡単な ICMP ソケット操作であれば、アプリの実装にマッチする駆動方式を選定すればよいと考えます。
しかし、例えば以下のように細かな制御が必要であれば POSIX Socket の一択になるでしょう。

  • ソケットオプション SO_TIMESTAMP を設定し、
  • Echo reply パケットのタイムスタンプ (正確な受信時刻) をカーネルから取得する

付録 A: C言語による実装例 (POSIX Socket)

one_ping.c
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <poll.h>

#define HOST_NAME	"ifconfig.io"
#define IP_ADDRESS	"104.24.122.146"
#define ECHO_ID		12345
#define ECHO_SEQ	0
#define DGRAM_SIZE	64

int main() {
	int sockfd, ret, cklen, sum, ip_hlen;
	unsigned short *sp;
	double rtt;
	char sendbuf[BUFSIZ], recvbuf[BUFSIZ];
	struct sockaddr_in to, from;
	socklen_t fromlen;
	size_t sent, received;
	struct timeval send_tv, recv_tv;
	struct ip *ip_hdr;
	struct icmp *icmp_hdr;
	struct pollfd fds[1];
	
	/**********************************/
	/* Step 1. ICMP ソケットを作成    */
	/**********************************/
	if ((sockfd = socket(PF_INET, SOCK_DGRAM, IPPROTO_ICMP)) == -1) {
		perror("socket");
		exit(1);
	}
	
	/**********************************/
	/* Step 2. 宛先アドレスを作成     */
	/**********************************/
	memset(&to, 0, sizeof(to));
	to.sin_family = AF_INET;
	to.sin_addr.s_addr = inet_addr(IP_ADDRESS);
	to.sin_port = htons(0);
	
	/**********************************/
	/* Step 3. 送信データグラムを作成 */
	/**********************************/
	memset(sendbuf, 0, sizeof(sendbuf));
	icmp_hdr = (struct icmp *)sendbuf;
	icmp_hdr->icmp_type = ICMP_ECHO;
	icmp_hdr->icmp_code = 0;
	icmp_hdr->icmp_cksum = 0;
	icmp_hdr->icmp_id = htons(ECHO_ID);
	icmp_hdr->icmp_seq = htons(ECHO_SEQ);
	
	/* チェックサム計算 */
	cklen = DGRAM_SIZE;
	sum = 0;
	sp = (unsigned short *)icmp_hdr;
	while (cklen > 1) {
		sum += *sp++;
		cklen -= 2;
	}
	if (cklen == 1)
		sum += (*sp >> 1);
	sum = (sum >> 16) + (sum & 0xffff);
	sum += (sum >> 16);
	icmp_hdr->icmp_cksum = ~sum;
	
	/**********************************/
	/* Step 4. Echo request を送信    */
	/**********************************/
	if (gettimeofday(&send_tv, NULL) == -1) {
		perror("gettimeofday");
		exit(1);
	}
	if ((sent = sendto(sockfd, sendbuf, DGRAM_SIZE, 0, (struct sockaddr *)&to, sizeof(to))) == -1) {
		perror("sendto");
		exit(1);
	}
	printf("PING %s (%s): %ld data bytes\n", HOST_NAME, IP_ADDRESS, (sent - sizeof(icmp_hdr)));
	
	/**********************************/
	/* Step 5. 応答を待ち合わせ       */
	/**********************************/
	memset(&fds, 0, sizeof(fds));
	fds[0].fd = sockfd;
	fds[0].events = POLLIN;
	while (1) {
		ret = poll(fds, 1, 1000);
		if (ret == -1) /* おそらくEINTER */
			continue;
		if (ret == 0) { /* タイムアウト */
			printf("Request timeout\n");
			exit(1);
		}
		break;
	}
	if ((fds[0].revents & POLLIN) != POLLIN) { /* POLLERR, POLLHUP または POLLNVAL のいずれかが発生 */
		printf("poll failed\n");
		exit(1);
	}
	
	/**********************************/
	/* Step 6. Echo reply を受信      */
	/**********************************/
	memset(recvbuf, 0, sizeof(recvbuf));
	memset(&from, 0, sizeof(from));
	fromlen = sizeof(from);
	received = recvfrom(sockfd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&from, &fromlen)
	if (received == -1) {
		perror("recvfrom");
		exit(1);
	}
	if (gettimeofday(&recv_tv, NULL) == -1) {
		perror("gettimeofday");
		exit(1);
	}
	if (received == 0) {
		printf("ICMP reply: no data\n");
		exit(1);
	}
	ip_hdr = (struct ip*)recvbuf;
	ip_hlen = (ip_hdr->ip_hl << 2);  /* 実際のIPヘッダ長 */
	if (received < ip_hlen + ICMP_MINLEN) {
		printf("ICMP reply: too short\n");
		exit(1);
	}
	icmp_hdr = (struct icmp *)(recvbuf + ip_hlen);
	if (icmp_hdr->icmp_type != ICMP_ECHOREPLY) {
		printf("ICMP reply: message(type: %d) received\n", icmp_hdr->icmp_type);
		exit(1);
	}
	if (htons(icmp_hdr->icmp_id) != ECHO_ID) {
		printf("ICMP reply: id invalid\n");
		exit(1);
	}
	rtt = ((double)recv_tv.tv_sec) * 1000.0 + ((double)recv_tv.tv_usec) / 1000.0;
	rtt -= ((double)send_tv.tv_sec) * 1000.0 + ((double)send_tv.tv_usec) / 1000.0;
	printf("%ld bytes from %s: icmp_seq=%d ttl=%d time=%.3f ms\n", (received - ip_hlen), inet_ntoa(from.sin_addr), htons(icmp_hdr->icmp_seq), ip_hdr->ip_ttl, rtt);
	
	if (close(sockfd) == -1) {
		perror("close");
		exit(1);
	}
	exit(0);
}

付録 B: Swift による実装例 (POSIX Socket)

(2020/12/18 追記)
ブリッジングヘッダーを使用する前のソースコードです。

POSIXSocket_OnePing.playground
import Darwin
import Foundation

let HOST_NAME             = "ifconfig.io"
let IP_ADDRESS            = "104.24.122.146"
let ECHO_ID: UInt16       = 12345
let ECHO_SEQ: UInt16      = 0
let IP_HDRLEN             = 20
let ICMP_HDRLEN           = 8
let ICMP_ECHOREPLY: UInt8 = 0
let ICMP_ECHO: UInt8      = 8

struct ip {  // 20 bytes
    let ip_vhl: UInt8      // Version + Header length
    let ip_tos: UInt8      // Type of service
    let ip_len: UInt16     // Total length
    let ip_id: UInt16      // Identification
    let ip_offset: UInt16  // Fragment offset field
    let ip_ttl: UInt8      // Time to live
    let ip_proto: UInt8    // IP protocol
    let ip_cksum: UInt16   // Checksum
    let ip_src: in_addr    // Source address
    let ip_dst: in_addr    // Destination address
}

struct icmpEchoHdr {  // 8 bytes
    var icmp_type: UInt8        // Type of message
    var icmp_code: UInt8 = 0    // Sub code
    var icmp_cksum: UInt16 = 0  // Cksum of ICMP datagram
    var icmp_id: UInt16         // Identification
    var icmp_seq: UInt16        // Sequence
}

struct echoRequest {  // 64 bytes
    var icmpHdr: icmpEchoHdr
    var payload: (Int64,Int64,Int64,Int64,Int64,Int64,Int64) = (0,0,0,0,0,0,0)
}

func main() {
    //============================
    // Step 1. ICMP ソケットを作成
    //============================
    let sockfd = Darwin.socket(PF_INET, SOCK_DGRAM, IPPROTO_ICMP)
    guard sockfd != -1 else {
        print("socket: " + String(cString: strerror(errno)))
        return
    }
    
    //============================
    // Step 2. 宛先アドレスを作成
    //============================
    var to = sockaddr_in(sin_len: UInt8(MemoryLayout<sockaddr_in>.size),
                         sin_family: UInt8(AF_INET),
                         sin_port: in_port_t(0).bigEndian,
                         sin_addr: in_addr(s_addr: inet_addr(IP_ADDRESS)),
                         sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
    
    //============================
    // Step 3. 送信データグラムを作成
    //============================
    var echo = echoRequest(icmpHdr: icmpEchoHdr(icmp_type: ICMP_ECHO, icmp_id: ECHO_ID.bigEndian, icmp_seq: ECHO_SEQ.bigEndian))
    
    // チェックサム計算
    let typecode = Data([echo.icmpHdr.icmp_type, echo.icmpHdr.icmp_code]).withUnsafeBytes { $0.load(as: UInt16.self) }
    var sum = UInt64(typecode) + UInt64(echo.icmpHdr.icmp_id) + UInt64(echo.icmpHdr.icmp_seq)
    while sum >> 16 != 0 {
        sum = (sum & 0xffff) + (sum >> 16)
    }
    echo.icmpHdr.icmp_cksum = ~UInt16(sum)
    
    //============================
    // Step 4. Echo request を送信
    //============================
    let sentDate = Date()
    let sendData = Data(bytes: &echo, count: MemoryLayout<echoRequest>.size)
    let tolen = socklen_t(to.sin_len)
    let sent = withUnsafePointer(to: &to) { sockaddr_in in
        sockaddr_in.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddr in
            sendData.withUnsafeBytes { (pointer: UnsafeRawBufferPointer) -> size_t in
                let unsafeBufferPointer = pointer.bindMemory(to: UInt8.self)
                return Darwin.sendto(sockfd, unsafeBufferPointer.baseAddress, sendData.count, 0, sockaddr, tolen)
            }
        }
    }
    guard sent != -1 else {
        print("sendto: " + String(cString: strerror(errno)))
        return
    }
    print("PING \(HOST_NAME) (\(IP_ADDRESS)): \(sent - ICMP_HDRLEN) data bytes")
    
    //============================
    // Step 5. 応答を待ち合わせ
    //============================
    var fds = [ pollfd(fd: sockfd, events: Int16(POLLIN), revents: 0) ]
    while true {
        let ret = Darwin.poll(&fds, 1, 1000)
        if ret == -1 {  // おそらくEINTER
            continue
        }
        if ret == 0 {  // タイムアウト
            print("Request timeout")
            return
        }
        if Int32(fds[0].revents) & POLLIN != POLLIN {
            print("poll failed")  // POLLERR, POLLHUP または POLLNVAL のいずれかが発生
            return
        }
        break
    }
    
    //============================
    // Step 6. Echo reply を受信
    //============================
    var recvData = Data([UInt8](repeating: 0, count: Int(BUFSIZ)))
    let dataSize = recvData.count
    var from = sockaddr_in()
    var fromlen = socklen_t(MemoryLayout<sockaddr_in>.size)
    let received = withUnsafeMutablePointer(to: &from) { sockaddr_in in
        sockaddr_in.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddr in
            recvData.withUnsafeMutableBytes { (pointer: UnsafeMutableRawBufferPointer) -> size_t in
                let unsafeBufferPointer = pointer.bindMemory(to: UInt8.self)
                return Darwin.recvfrom(sockfd, unsafeBufferPointer.baseAddress, dataSize, 0, sockaddr, &fromlen)
            }
        }
    }
    guard received != -1 else {
        print("recvfrom: " + String(cString: strerror(errno)))
        return
    }
    let recvDate = Date()
    guard received > 0 else {
        print("ICMP reply: no data")
        return
    }
    let ipHdr = Data(recvData[0 ..< IP_HDRLEN]).withUnsafeBytes { $0.load(as: ip.self) }
    let ipHdrLen = Int((ipHdr.ip_vhl & 0xF) << 2)  // 実際のIPヘッダ長
    guard received >= ipHdrLen + ICMP_HDRLEN else {
        print("ICMP reply: too short")
        return
    }
    let icmpHdr = Data(recvData[ipHdrLen ..< ipHdrLen + ICMP_HDRLEN]).withUnsafeBytes { $0.load(as: icmpEchoHdr.self) }
    guard icmpHdr.icmp_type == ICMP_ECHOREPLY else {
        print("ICMP reply: message(type: \(icmpHdr.icmp_type)) received")
        return
    }
    guard icmpHdr.icmp_id.bigEndian == ECHO_ID else {
        print("ICMP reply: id invalid")
        return
    }
    var msg = "\(received - ipHdrLen) bytes "
    msg += "from \(String.init(cString: inet_ntoa(from.sin_addr))): "
    msg += "icmp_seq=\(icmpHdr.icmp_seq.bigEndian) "
    msg += "ttl=\(ipHdr.ip_ttl) "
    msg += String(format: "time=%.3f ms", recvDate.timeIntervalSince(sentDate) * 1000)
    print(msg)
    
    guard Darwin.close(sockfd) != -1 else {
        print("close: " + String(cString: strerror(errno)))
        return
    }
}

main()

付録 C: Swift による実装例 (CFSocket)

(2020/12/18 追記)
ブリッジングヘッダーを使用する前のソースコードです。

CFSocket_OnePing.playground
import CoreFoundation
import Foundation

let HOST_NAME             = "ifconfig.io"
let IP_ADDRESS            = "104.24.122.146"
let ECHO_ID: UInt16       = 12345
let ECHO_SEQ: UInt16      = 0
let IP_HDRLEN             = 20
let ICMP_HDRLEN           = 8
let ICMP_ECHOREPLY: UInt8 = 0
let ICMP_ECHO: UInt8      = 8

struct ip {  // 20 bytes
    let ip_vhl: UInt8      // Version + Header length
    let ip_tos: UInt8      // Type of service
    let ip_len: UInt16     // Total length
    let ip_id: UInt16      // Identification
    let ip_offset: UInt16  // Fragment offset field
    let ip_ttl: UInt8      // Time to live
    let ip_proto: UInt8    // IP protocol
    let ip_cksum: UInt16   // Checksum
    let ip_src: in_addr    // Source address
    let ip_dst: in_addr    // Destination address
}

struct icmpEchoHdr {  // 8 bytes
    var icmp_type: UInt8        // Type of message
    var icmp_code: UInt8 = 0    // Sub code
    var icmp_cksum: UInt16 = 0  // Cksum of ICMP datagram
    var icmp_id: UInt16         // Identification
    var icmp_seq: UInt16        // Sequence
}

struct echoRequest {  // 64 bytes
    var icmpHdr: icmpEchoHdr
    var payload: (Int64,Int64,Int64,Int64,Int64,Int64,Int64) = (0,0,0,0,0,0,0)
}

class SocketContext {
    var onData: ((NSData) -> Void)?
}

var waitingReply = false
var socketContext = SocketContext()
socketContext.onData = { (data: NSData) in
    //============================
    // Step 6. Echo reply を受信
    //============================
    DispatchQueue.main.async {
        waitingReply = false
        let recvDate = Date()
        guard data.count > 0 else {
            print("ICMP reply: no data")
            return
        }
        let ipHdr = Data(data[0 ..< IP_HDRLEN]).withUnsafeBytes { $0.load(as: ip.self) }
        let ipHdrLen = Int((ipHdr.ip_vhl & 0xF) << 2)  // 実際のIPヘッダ長
        guard data.count >= ipHdrLen + ICMP_HDRLEN else {
            print("ICMP reply: too short")
            return
        }
        let icmpHdr = Data(data[ipHdrLen ..< ipHdrLen + ICMP_HDRLEN]).withUnsafeBytes { $0.load(as: icmpEchoHdr.self) }
        guard icmpHdr.icmp_type == ICMP_ECHOREPLY else {
            print("ICMP reply: message(type: \(icmpHdr.icmp_type)) received")
            return
        }
        guard icmpHdr.icmp_id.bigEndian == ECHO_ID else {
            print("ICMP reply: id invalid")
            return
        }
        var msg = "\(data.count - ipHdrLen) bytes "
        msg += "from \(String.init(cString: inet_ntoa(ipHdr.ip_src))): "
        msg += "icmp_seq=\(icmpHdr.icmp_seq.bigEndian) "
        msg += "ttl=\(ipHdr.ip_ttl) "
        msg += String(format: "time=%.3f ms", recvDate.timeIntervalSince(sentDate) * 1000)
        print(msg)
    }
}
var cfSocketContext = CFSocketContext(version: 0,
                                      info: &socketContext,
                                      retain: nil,
                                      release: nil,
                                      copyDescription: nil)
func callout(sock: CFSocket?, callbackType: CFSocketCallBackType, address: CFData?, _data: UnsafeRawPointer?, _info: UnsafeMutableRawPointer?) -> Void {
    switch callbackType {
    case CFSocketCallBackType.dataCallBack:
        let info = _info!.assumingMemoryBound(to: SocketContext.self).pointee
        let data = Unmanaged<CFData>.fromOpaque(_data!).takeUnretainedValue()
        info.onData?(data)
    default:
        break
    }
}

//============================
// Step 1. ICMP ソケットを作成
//============================
let socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_DGRAM, IPPROTO_ICMP,
                            CFSocketCallBackType.dataCallBack.rawValue,
                            callout,
                            &cfSocketContext)

//============================
// Step 2. 宛先アドレスを作成
//============================
var to = sockaddr_in(sin_len: UInt8(MemoryLayout<sockaddr_in>.size),
                     sin_family: UInt8(AF_INET),
                     sin_port: in_port_t(0).bigEndian,
                     sin_addr: in_addr(s_addr: inet_addr(IP_ADDRESS)),
                     sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))

//============================
// Step 3. 送信データグラムを作成
//============================
var echo = echoRequest(icmpHdr: icmpEchoHdr(icmp_type: ICMP_ECHO, icmp_id: ECHO_ID.bigEndian, icmp_seq: 0))

// チェックサム計算
let typecode = Data([echo.icmpHdr.icmp_type, echo.icmpHdr.icmp_code]).withUnsafeBytes { $0.load(as: UInt16.self) }
var sum = UInt64(typecode) + UInt64(echo.icmpHdr.icmp_id) + UInt64(echo.icmpHdr.icmp_seq)
while sum >> 16 != 0 {
    sum = (sum & 0xffff) + (sum >> 16)
}
echo.icmpHdr.icmp_cksum = ~UInt16(sum)

//============================
// Step 4. Echo request を送信
//============================
let address = NSData(bytes: &to, length: MemoryLayout<sockaddr_in>.size)
let sendData = NSData(bytes: &echo, length: MemoryLayout<echoRequest>.size)
let sentDate = Date()
let error = CFSocketSendData(socket, address as CFData, sendData as CFData, 1)

switch error {
case .success:
    print("PING \(HOST_NAME) (\(IP_ADDRESS)): \(sendData.count - ICMP_HDRLEN) data bytes")
    
    //============================
    // Step 5. 応答を待ち合わせ
    //============================
    let thread = Thread.init {
        CFRunLoopAddSource(CFRunLoopGetCurrent(),
                           CFSocketCreateRunLoopSource(nil, socket, 1),
                           CFRunLoopMode.defaultMode)
        RunLoop.current.run()
    }
    waitingReply = true
    thread.start()  // スレッド開始
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        thread.cancel()  // スレッド停止
        if waitingReply {  // タイムアウト
            print("Request timeout")
        }
        return
    }
case .error:
    print("Error occurred in send request")
case .timeout:
    print("Send timed out")
}

終わり。

  1. Internet Control Message Protocol (インターネット制御通知プロトコル)

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?