概要: この記事では、アプリ開発者の役に立ちそうな TCP/IPネットワークに関する最低限の知識をまとめた。 あくまで視点は「アプリ開発者」なので、 現在の家庭・企業における一般的なネットワーク環境しか想定していないし、 セキュリティに関しても基本的なことしかカバーしていない。 ネットワーク機器の具体的な設定方法や診断については、より専門的な資料を参照のこと。
目的: この記事を理解すると、 まとめ問題1、 まとめ問題2 および NATの動きを理解する のような問題に答えることができる。 (逆に、現時点でこれができる人には本記事は不要。)
以下のツールをインストールして使うこと:
これを実現するには、2通りの方法がある:
インターネットではパケット通信を使っている。
ネットワークは構築するのが大変だ。(道路と同じ)
解決策: いくつかの取り替え可能な「部品」から作る。(ソフトウェアと同じ)
あるレイヤーを使っている人は、その下の階層を知らなくてもよい。 (そのため揶揄的に“土管”などと呼ばれる。)
OSI はネットワーク規格としては普及しなかったが、 その階層モデルはいまだ説明用によく言及されている。
インターネットではデータリンク層と物理層を区別していない。 また、アプリケーション層から上は「使う人が勝手に決める」。
注意: インターネットの階層でハードウェアが関連しているのは おもにデータリンク層だけである。それ以上の階層は、 すべてソフトウェア的な「部品」であり、 現在は OS の機能の一部として提供されている。
テレビなどと比較すると、インターネットでは上の要素はどれも可変である。 唯一の規格は送受信の方式だけ。そのため、 インターネットは「自由なネットワークだ」といわれる。
すべての通信では、まずデータ (0/1 の列) をどうにかして 別のコンピュータに送らねばならない。
ひとつ隣のホストは別の国である。
どうにかして 0と1 さえ送れれば、電気を使わなくてもかまわない: http://www.blug.linux.no/rfc1149/
IP = Internet Protocol
インターネット上のある「地点」は IPアドレス によって表現される。 IPアドレスは物理的な場所とは何の関係もない。
127.0.0.1
58.158.55.222
255.255.255.255
IPパケットは、複数台のコンピュータを経由して転送される。 このような中継用のコンピュータを ルータ (router) とよぶ。
TCP = Transmission Control Protocol
TCP 層によって、インターネット上のあるホストから別のホストに 「任意の長さの 0 と 1 の列」を送れるようになる。
最後にアプリケーション層が、送られてきたデータを ユーザにとって意味のある方法で利用する。
最近では、この短所を解決するため、さらにこの上に TLS/SSL層などを導入することもある (HTTPSなど)。
08-00-27-16-f3-c3
のように 6組の 16進数で表される。
自分のパソコンについている イーサネット の MAC アドレスを調べよ。 (複数のインターフェイスが接続されている場合、MAC アドレスは複数存在することがある。)
イーサネット を使って別のノードにデータを送るときは、 以下のような順序で 0 と 1 を送ることになっている。 なぜか イーサネット では「パケット」ではなく フレーム (frame) と呼ばれている。
10101010 | … | 10101011 | 送信先MACアドレス (6バイト) |
送信元MACアドレス (6バイト) |
データ長 (2バイト) |
実際のデータ (46〜1500バイト) |
CRC (4バイト) |
00000000 |
一般的に、このような決まりを プロトコル (Protocol, 規約) とよぶ。
以下の Ethernet フレームについて問いに答えよ:
06 e6 76 53 06 c8 04 e6 76 53 06 c8 00 05 11 22 33 44 55 aa bb cc dd
Wireshark を起動後にパケットキャプチャーを開始し、 各パケットの詳細パネルをクリックして イーサネット の送信元・送信先MACアドレスを確認せよ。
ふつう「ネットワークインターフェイス」といえば、 イーサネット などの物理的な接続口をさすが、最近ではソフトウェアによる 「仮想的な」ネットワークインターフェイスも存在する。
自分の PC についている仮想インターフェイスはあるか?
ifconfig
コマンドで確認せよ。
$ ifconfig
データリンク層の機能を使うと、小口のデータを信頼できる方法で 隣の (直接接続されている) ホストに送信できることがわかった。 データリンク層では基本的にどんなデータを送ってもよいが、 たいていは IPパケット の内容が送られている。 これをバケツリレー式に転送していくのが IP層 (ネットワーク層) の役割である。
10101010 | … | 10101011 | 送信元MACアドレス | 送信先MACアドレス | データ 長 | IPパケット | CRC | 00000000 |
バイト | 0 | 1 | 2 | 3 |
---|---|---|---|---|
0 | バージョン/ヘッダ長 | タイプ | IPパケット長 | |
4 | 識別子 | フラグ | ||
8 | TTL | プロトコル番号 | チェックサム | |
12 | 送信元IPアドレス | |||
16 | 送信先IPアドレス | |||
20 | オプション | パディング | ||
24 | 実際のデータ … |
以下の緑色部分はイーサネット フレームに含まれる 実際の IPパケットを表したものである。 上の図を参考にして、以下の情報を求めよ:
0000 94 09 37 28 5b d5 00 23 81 1a f2 8d 08 00 45 00
0010 00 34 6b 7f 40 00 80 06 00 00 c0 a8 00 0c c0 a8
0020 01 01 c9 34 00 17 25 d0 1e c0 00 00 00 00 80 02
0030 20 00 82 84 00 00 02 04 05 b4 01 03 03 08 01 01
0040 04 02
Wireshark を起動後、パケットキャプチャーを開始し、
フィルタバーに ip
と入力せよ。
各パケットをクリックして送信元・送信先のIPアドレスを確認せよ。
すべての IPパケットには、送信元・送信先のIPアドレスがついている。 これはふつう 4つの 8ビット数 (0〜255) の組として表記する。
127.0.0.1
58.158.55.222
255.255.255.255
343.45.687.90
(間違い)
自分のパソコンについている IP アドレスを調べよ。 (複数のインターフェイスが存在する場合、 ひとつの PC が複数の IP アドレスを名乗っていることがある。)
ping
コマンドは、与えられた IP アドレスの
ホストが起動しているか (IP パケットに応答できるか) 否かを調べるものである。
以下のホストに ping
コマンドを実行して、
応答があるかどうかを確認せよ:
$ ping 自分のIPアドレス $ ping 127.0.0.1 $ ping 192.0.2.1 $ ping 1.1.1.1
MACアドレスは通常固定なのに対し、IPアドレスは可変である。 IPアドレスがなければ、そもそも IP層を使って通信できないため、 すべての PC は起動時に IPアドレスを決定する必要がある。 現在、IPアドレスを決める方法はおもに 3通り存在する:
192.168.1.100
〜 192.168.1.199
など)
のアドレスの中から、一定時間使われていないものを選ぶ。
家庭用ルータの設定画面にログインし、DHCPアドレスの範囲を調べよ。 そのルータを使っているデバイスの IP アドレスを調べ、 指定する範囲内にあることを確認せよ。
IPパケットは、ふつう複数のコンピュータを中継して送信される。
(注意: 米国英語の "route" / "router" はどちらかというと 「ラウト」「ラウター」に近い発音だが、ここでは日本語の慣例にならって 「ルート」「ルーティング」と表記する。)
ルータは受けとったパケットをいったんメモリに保管し、 適切な送り先に向けて送信する。
1つの回線 (ISP) からくるパケットを複数台のPCに転送する。 たいていの家庭用ルータには、単なるルーティング機能に加えて、後述する ファイヤウォール、NAT、DHCPサーバ およびウェブサーバなどの機能が搭載されている。
複数の基幹回線とつながっており、ISPなどで使われる。 Cisco 8000 など。 10Tbps〜200Tbps の処理能力があり、非常に高価。
MACアドレスは各機器に固有なのに対して、 IPアドレスは各ホストが「勝手に名乗っているだけ」であり、しかもつねに変化している。 つまりルータは世界のどこにいるかわからないホストに対して、 IPパケットを送らなければならない。 しかも、インターネットの正確な全体像はわからない。
小さな組織 (自宅・会社・学校) で、ネットワークが 階層構造 (ツリー構造) になっているときに使える方法。
各ホストは、ルーティングテーブルと呼ばれる情報を持っている。 ここには以下のようなルールが記載されている:
if 送り先 が ある範囲の IPアドレスならば インターフェイス1を使う else if 送り先 が ある範囲の IPアドレスならば インターフェイス2を使う ... else インターフェイスNを使う
ここでいう「ある範囲の IPアドレス」のことを、 サブネットワークまたは サブネット (subnet) という。
末端のホストは、ひとつのサブネットに接続している。 これは次のような方法でIPパケットを転送する:
ルータは 2つ以上のサブネットに接続しており、 少なくとも2つのIPアドレスを持っている。 ルータは次のような方法でIPパケットを転送する:
あるIPアドレスが特定の「サブネット」に所属しているかどうかは、 以下のようにして決定する:
IP アドレス | 11110000 10101000 00000001 00000010 |
(192.168.1.2) |
---|---|---|
サブネット・マスク | 11111111 11111111 11111111 00000000 |
(255.255.255.0) |
ネットワーク・アドレス | 11110000 10101100 00000001 00000000 |
(192.168.1.0) |
通常、サブネット・マスクは IP アドレスと同様に表記する。
しかし、単に 1の個数を「/24
」のように表すことも多い。
255.255.255.0
/24
以下の IP アドレスに対するネットワークアドレスを求めよ:
10.0.1.3/8
128.122.100.181/16 (255.255.0.0)
192.168.1.9/24 (255.255.255.0)
166.84.7.55/30 (255.255.255.252)
あるルータが以下のIPアドレスをもっている。 これらは2つのサブネットに接続しており、 そのうちのひとつはデフォルト・ゲートウェイである。
10.0.2.2/16
(サブネットA)
192.168.3.1/24
(サブネットB)
10.0.2.1
以下のIPパケットが来た場合、ルータはパケットを どのように転送すればよいか?
10.0.3.4
→ 送信先: 192.168.3.2
192.168.3.3
→ 送信先: 10.0.3.1
192.168.3.3
→ 送信先: 10.1.3.1
ネットワークがさらに複雑な場合、 もはや単純な規則では行き先を決定できない。 このような広域用のルータは、インターネットの「地図」のようなもの (ルーティング情報) を持っている。 これはつねに変化するため、たえず更新されている。 (ちなみに、この情報も TCP/IP ネットワーク自身を使って送られる。)
中規模の組織で利用される。各ルータは、ネットワーク上のホストが どのように接続されているか定期的に調査し、ネットワークの「地図」を作成する。 ここから各ホスト間の最短経路を計算する。
さらに大規模なルーティングに使われる方式。 インターネットを各自律システム (Autonomous System, AS) に分け、 AS 間での経路情報を交換する。世界には約5万のASがあり、相互に協力している。
各ASは BGP (Border Gateway Protocol) という手順で 「このネットワークはこっち」という経路情報を交換している。 しかし全インターネットの正確な地図を把握できるわけではなく 「だいたいこっちの方に送れば正しいだろう」程度の情報しかわからない。
業務用ルータ1台は、通常のパソコン数百台分の性能をもっている。 アドレスの照合のために、TCAMという特別なメモリが使われている。
ニューヨークに存在する、とあるサーバの IP アドレスは 166.84.7.55 である。
traceroute
コマンド (Windows の場合は、tracert
コマンド) を使って、
ここに行くまでの経路を調べよ。
macOS/Linuxの場合:
$ traceroute 166.84.7.55
Windowsの場合:
C:\> tracert 166.84.7.55
IP層の機能を使うと、世界の離れた場所にパケットを送ることができる。 しかし…
各ルータはパケット正しく送るよう精一杯努力するが、これは ベストエフォート方式 (がんばったら、結果がダメでも許してもらえる) にすぎない。 そこで、信頼性のない IPパケットをうまく使って、 信頼できる、長い 0/1 の列 を送る仕組みを作る。 これが TCP 層である。
TCP層までくると、2つのホストは連続した通信ができるので、 これはもはやパケット交換ではなく、仮想的な1本の線とみなせる。
TCP層は IP層を利用している。 つまり IPパケットの中に、さらに TCPパケット (TCPセグメント) を入れる。
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | 送り元ポート | 送り先ポート | ||
4 | シーケンス番号 | |||
8 | 確認応答(ACK)番号 | |||
12 | フラグ | ウィンドウサイズ | ||
16 | チェックサム | 緊急ポインタ | ||
20 | オプション | |||
24 | 実際のデータ … |
TCP では通信はつねに双方向である。 2つのホストは (たとえ送るデータがないときでも) つねに次の情報を相手に送っている:
seq, ack はそれぞれ送った (受けとった) データのバイト数に応じて増加する:
ホストA | ホストB |
---|---|
[seq=1, ack=1, (10バイト)] → seq +10 |
ack +10 |
ack +5 | ← [seq=1, ack=11, (5バイト)] seq +5 |
[seq=11, ack=6, (0バイト)] → seq +0 |
ack +0 |
ack +3 | ← [seq=6, ack=11, (3バイト)] seq +3 |
[seq=11, ack=9, (1バイト)] → seq +1 |
ack +1 |
ack +0 | ← [seq=9, ack=12, (0バイト)] seq +0 |
送り側は、確認応答が来なかったら、以前に送ったパケットを再送信する。
TCP 通信は、開始時に特別な処理をおこなう。 この処理を 3ウェイ・ハンドシェイク と呼び、 TCP SYN という目印のついたパケットが使われる。
ハンドシェイクが完了すると、2つのホスト間は仮想回線で結ばれた状態となり (接続状態)、 双方向の通信が可能となる。ひとつの仮想回線では、次のものが決まっている:
IP層においては、すべてのホストはパケットを送受信するという意味で等価であった。
TCP層の通信では、必ず2種類の役割が存在する:
TCPサーバは、特定のTCPポートに TCP SYN が送られるかどうかを ずっと監視していなければならない。このような状態を 「Listen (待ち受け) 状態」という。
自分のPC上で動いているTCPクライアント、TCPサーバを表示してみよう。
Linuxの場合:
$ netstat -n -a -t -p
macOSの場合:
$ lsof -n -P -iTCP
TCP 層の機能は、現代のほとんど OS にデフォルトで搭載されている。 そのためアプリケーション側は「どのポートを listen したいか」 「どのポートに接続したいか」を指定するだけでよい。
$ nc -l 10000
$ nc サーバIPアドレス 10000
netstat
コマンドを実行すると、現在その PC上で
Listen/Established状態になっている TCPポート一覧を見ることができる。
上の演習 を実行中に、 サーバ・クライアントの両方で別のウィンドウを開き、 以下のコマンドを実行して、現在通信中のポートが表示されていることを確認せよ。
Linuxの場合:
$ netstat -n -t
macOSの場合:
$ lsof -n -P -iTCP
Wireshark を起動、パケットキャプチャーを開始し、
フィルタバーに ip.addr == サーバIPアドレス
と入力せよ。
その後、 上の演習 をもう一度くりかえし
TCP の 3ウェイハンドシェイクが行われていることを確認せよ。
TCP層は便利だが、いくつか欠点がある:
これに対して、TCP層のかわりに UDP (User Datagram Protocol) 層というものを使うこともできる。 これはIP層を使っているが、 TCPとは別の方法で通信をおこなう。
0 | 1 | 2 | 3 | |
---|---|---|---|---|
0 | 送り元ポート | 送り先ポート | ||
4 | データ長 | チェックサム | ||
8 | 実際のデータ … |
TCP/IPのデータリンク層・IP層・TCP層はすべて OS が提供している。 アプリケーション層は、インターネットの機能を実際に活用する部分であり、 ほとんどのアプリ開発者は「アプリケーション層」を作ることになる。
実際のアプリケーション層のデータは、Ethernetフレームの中の、 IPパケットの中の、TCPセグメントのさらに中に記録されている。
一般的に、もっともよく使われるのは TCP層の機能である。 現在の OS では「ソケット (socket)」という API が整備されており、 たとえば Python なら、すぐに TCP のサーバ-クライアント間で通信ができる:
from socket import * # TCP用ソケットを準備。 sock = socket(AF_INET, SOCK_STREAM) # TCPポート 10000番で listen状態にする。 sock.bind(('0.0.0.0', 10000)) sock.listen(1) # 接続してきたクライアント用のソケットを取得する。 (client, addr) = sock.accept() # データを受信する。 data = client.recv(10) print(data)
from socket import * # TCP用ソケットを準備。 sock = socket(AF_INET, SOCK_STREAM) # ホスト 127.0.0.1 の TCPポート 10000番に接続する。 sock.connect(('127.0.0.1', 10000)) # データを送信する。 sock.send(b'hello')
上の 2つの Pythonスクリプト
server.py
、client.py
を
同一のホスト上で同時に動かし、通信ができていることを確認せよ。
IP アドレスはそのままでは人間にとって覚えにくい。 そこで DNS (Domain Name System) という仕組みが用意されている。
ping
, traceroute
, nc
など) は
デフォルトで DNS に対応しており、これらのコマンドには
IPアドレスの代わりにホスト名を指定してもよい。
$ ping 166.84.7.55 $ ping www.tabesugi.net $ ping vc55.tabesugi.net
$ host 166.84.7.55 55.7.84.166.in-addr.arpa domain name pointer vc55.tabesugi.net.
DNS は TCP/IP の機能を使って作られている。
www.example.com
というホスト名は
www
、.example
、.com
という 3つの部分に分割される。
このようなサーバを用意したあとで、
www.example.com
の IPアドレスを求めるには:
198.41.0.4
) に
.com
ネームサーバのIPアドレスを問い合わせる →
192.5.6.30
.com
ネームサーバに
.example.com
ネームサーバのIPアドレスを問い合わせる →
199.43.133.54
.example.com
ネームサーバに
www.example.com
のIPアドレスを問い合わせる →
93.184.216.34
127.0.0.1
は
特別な IPアドレスであり、これはつねに「自分自身のホスト」をあらわす。
これをループバックアドレスという。
なお、ホスト名 localhost
は
つねにループバックアドレスに解決される。
$ ping localhost PING localhost (127.0.0.1) 56(84) bytes of data. 64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.029 ms 64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.085 ms 64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.084 ms ...
DNS はインターネットの基幹をなすサービスであり、 ほとんどの API やアプリ、サービスなどが DNS に依存している。 DNS が使えなければ、相手の IPアドレスがわからないので、 そもそも通信ができないことが多い。 そのため DNS がうまく動かない場合、 多くのサービスやサイトで大規模な障害が発生する。 これを防ぐため、ネームサーバは多重化されていることが多い。
$ ping www.tabenasugi.com ping: www.tabenasugi.com: Name or service not known
host
コマンドを使って、
自分がよく知っているホスト名の IPアドレスを求めよ。
(ping
でもよい)
$ host ホスト名
HTTP は、おそらくもっとも有名なアプリケーション層である。
通常、HTTP を使うクライアントを ウェブブラウザ (HTTPクライアント) と呼び、 それに答えるサーバを ウェブサーバ (HTTPサーバ) という。
たとえばブラウザで
http://www.example.com:80/news/
GET /news/ HTTP/1.1 Host: www.example.com User-Agent: Mozilla/5.0 (Windows NT 6.1;) Gecko/20100101 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,0/0;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive (空行)
これに対してサーバは以下のようなレスポンス文字列を返す:
HTTP/1.1 200 OK Connection: close Date: Fri, 20 Feb 2023 08:27:28 GMT Content-Type: text/html Content-Length: 9022 (空行) <html> ...
HTTP リクエストには GET, POST などいくつかの種類がある。 リクエストはさらに ヘッダ (header) と ボディ (ペイロード) (body, payload) に分かれている。 これらは空行で分けられている。
リクエストヘッダ:
GET /news/ HTTP/1.1 Host: www.example.com User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:35.0) Gecko/20100101 Firefox/35.0 … (空行)リクエストボディ (ペイロード):
ヘッダは 項目名: 値 の形をとる。
Host:
相手のサーバの名前
User-Agent:
ブラウザの種類
ボディは、GETリクエストの場合は空である (つまり何も含まれていない)。
HTTP レスポンスもまた、 ヘッダ (header) と ボディ (ペイロード) (body, payload) に分かれている。 これらは空行で分けられている。
レスポンスヘッダ:
HTTP/1.1 200 OK Connection: close Date: Fri, 20 Feb 2015 08:27:28 GMT Content-Type: text/html Content-Length: 9022 … (空行)レスポンスボディ (ペイロード):
<html><body> …
ヘッダは 項目名: 値 の形をとる。
Date:
サーバの応答時刻。
Content-Type:
そのコンテンツの種類 (HTMLファイルか画像か)。
Content-Length:
そのコンテンツの大きさ (バイト数)。
レスポンス内のペイロードには、通常 HTML ファイル (または画像ファイルなど) の内容がそのまま含まれている。 ブラウザはこの内容を解析して表示すると、ページが表示されたことになる。
正常なレスポンスの場合、最初の一行は必ず
となっているが、これ以外のレスポンスが返ってくる場合もある。HTTP/1.1 200 OK
HTTP/1.1 404 Not Found
HTTP/1.1 301 Moved
nc
コマンドを使って、
www.example.com サーバに
「人間HTTPクライアント」としてリクエストを送り、
サーバ側のレスポンスを確認せよ。
$ nc www.example.com 80 GET / HTTP/1.0 Host: www.example.com (サーバからのレスポンス) HTTP/1.0 200 OK Content-Type: text/html; charset=UTF-8 Date: Mon, 23 Jan 2023 13:39:22 GMT … <!doctype html> <html> <head> …
nc
コマンドを使って、
「人間HTTPサーバ」としてブラウザからの
リクエストに返答せよ。
まず nc
をサーバとして起動し、
ブラウザから http://localhost:8080/
という URLに接続してリクエストが送られるのを確認してから、
応答を入力せよ。
$ nc -l 8080 (クライアントからのリクエスト) GET / HTTP/1.1 User-Agent: Mozilla/5.0 … HTTP/1.0 200 OK Content-Type: text/plain Hello. ^C
あるユーザが「インターネットが動かない」といっている。
聞けばブラウザに http://www.example.com/
というアドレスを入力したが、
ページが表示されないのだという。この場合、考えられる原因をすべてあげよ。
HTTP はあまりにも普及しているため、 こんにち多くのアプリケーションは TCP層を直接使わず、 HTTP「層」を使って構築されるようになっている。 このようなアプリを Webアプリ と呼ぶ。 また、上で紹介した socket API に似た機構を HTTP層の上で実現した WebSocket という仕組みもある。
HTTP 以外のアプリケーション層の例として、SMTP (Simple Mail Transfer Protocol) がある。 これは特定のサーバ (メールサーバ) の TCP 25番ポートに データを送ると、電子メールを送ることができる。
$ nc gmail-smtp-in.l.google.com 25 220 mx.google.com ESMTP HELO mail.example.com 250 mx.google.com at your service MAIL FROM:<送り元メールアドレス> 250 2.1.0 OK RCPT TO:<xxx@gmail.com> 250 2.1.5 OK DATA 354 Go ahead zz-1234 - gsmtp From: <送り元メールアドレス> To: <xxx@gmail.com> Message-Id: <zz12345678@mail.example.com> Subject: test Hello, this is test! . 250 2.0.0 OK QUIT 221 2.0.0 closing connection
ping 93.184.216.34
とやると成功するのに、
ping www.example.com
ではエラーになる。http://test.example.com/foo/bar
を開いた時に送られるHTTPリクエストについて、
以下の空欄を埋めよ:
GET /foo/bar HTTP/1.1 Location: test.example.com User-Agent: Mozilla/5.0 ...
本来、TCP/IP の公式な範疇はTCP層までで、それより上は アプリケーションの責任となっている。しかし TCP 層では 真に安全な通信は実現できない:
最近では、これらの欠点を補うために TCP層の上にさらにもう一段、 (TCP/IPの範疇ではない) 暗号化をおこなう層を追加して使うことが多い。
暗号化通信において特に重要なこと:
なぜなら悪者も暗号は使えるからだ。 たとえば、中継するホストが正当な相手のフリをして、暗号を使って通信すれば、 結局のところ情報は悪者の手に渡ってしまう (中間者攻撃)。 したがって、暗号はつねに end-to-end で利用する必要がある。
現在普及している暗号化層:
TLS/SSL も SSH も (本来は TCP/IP 層の一部ではなくアプリケーション層なので)、 アプリケーションまたはライブラリの形で提供されている。 どちらもベースになっているのは公開鍵暗号技術である。
公開鍵暗号技術については、以下のことだけ覚えておけばよい:
Aさんは、自分用の秘密鍵と公開鍵のペアを所持している。 Bさんは Aさんに会ったことはないが、 ネット上でたまたま Aさんの公開鍵を入手した。
デジタル証明書にはサーバの公開鍵が記載されている。 デジタル証明書は認証局が正当性を保証するもので、 これもまた公開鍵暗号技術を使っている。 詳細は 「デジタル証明書とは何か?」を参照。
実際の TLS/SSL による通信は openssl
コマンドを使って
おこなう (なお、HTTPS では通常 TCP 443番ポートが使われる):
$ openssl s_client -connect www.example.com:443 CONNECTED(00000003) depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA verify return:1 depth=1 C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1 verify return:1 depth=0 C = US, ST = California, L = Los Angeles, O = Internet\C2\A0Corporation\C2\A0for\C2\A0Assigned\C2\A0Names\C2\A0and\C2\A0Numbers, CN = www.example.org verify return:1 ...
TLS/SSL層は以下のような手順で動作する:
以上が完了すると、TLS/SSL層は通常の TCP層と同じようにふるまう。 HTTPS の処理は、TLS/SSL層を使う部分以外は HTTP と同じである。
上で示した openssl コマンドを使って、 先の演習にならい サーバに HTTPリクエストを送り、応答を確認せよ。
$ openssl s_client -connect www.example.com:443 (サーバ証明書の表示) ... GET / HTTP/1.0 Host: www.example.com (サーバからのレスポンス) HTTP/1.0 200 OK Content-Type: text/html; charset=UTF-8 …
curl
コマンドを使うと、指定した URL に
HTTP/HTTPS リクエストを送り、レスポンスを画面に表示する。
nc
コマンドや
openssl
コマンドを使って手で直接 HTTP リクエストを入力するよりも、
こちらのほうが便利。
Webサーバの挙動を調べるのにおすすめ。
$ curl https://www.example.com/ <!doctype html> <html> <head> ...
-i
オプションをつけると、HTTP のレスポンスヘッダも表示する。
$ curl -i https://www.example.com/ HTTP/2 200 accept-ranges: bytes age: 49533 cache-control: max-age=604800 ...
-H
オプションを使えば、リクエストヘッダに任意の値を指定できる:
$ curl -i -H "If-Modified-Since: Sun, 1 Jan 2023 00:00:00 GMT" https://www.example.com/ HTTP/2 304 ...
上で示した curl
コマンドの使用例を実際に実行せよ。
TLS/SSL 層の大きな欠点は、コストがかかることである。 TLS を使うには、ルート認証局を頂点とする、中央集権化された、 全世界規模の 公開鍵基盤 (PKI) を構築する必要がある。 これには大きな手間がかかるうえに、認証局が信頼できない場合がある というリスクも抱えている (参考: 2011年デジノター事件)。
これに対して、SSH はもっと簡単な仕組みを使って暗号化された通信を実現している:
SSH はもともと Unixマシンの遠隔操作のために開発されたため、 暗号化通信に加えて、ユーザの認証処理もおこなう。 現在、SSH はサーバ管理などでアプリ開発者がよく利用するツールとなっている。
注意: SSH ではユーザの認証にも公開鍵を使っているため、 2種類の「公開鍵」が存在する:
~/.ssh/authorized_keys
ファイルに、自分の認証用の公開鍵を登録しておく。
ここでは典型的な例 (Unixサーバに SSH を使ってログインする) をあげる:
ssh-keygen
コマンドを使って、
あらかじめ認証用の秘密鍵・公開鍵ペアを生成しておく。
(これは最初の一度だけ実行すればよい。)ssh-keygen
は勝手に上書きしてしまうので注意。
client$ ssh-keygen -t ed25519 Generating public/private ed25519 key pair. Enter file in which to save the key (/home/euske/.ssh/id_ed25519): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/euske/.ssh/id_ed25519 (秘密鍵ファイル) Your public key has been saved in /home/euske/.ssh/id_ed25519.pub (公開鍵ファイル) ...
ssh-keygen
で入力するパスフレーズは秘密鍵の暗号化に使うものであって、サーバのログインに使うパスワードとは関連がない。~/.ssh/authorized_keys
にコピーしておく。
server.example.com
の公開鍵の指紋は
SHA256:HLfnjG5dDqVPIfWtG6sNGG5JyY5AYtkxaupGsJffyYs
であるとしよう。
ssh
コマンドを使ってサーバに接続する。client$ ssh user@server.example.com The authenticity of host '[server.example.com]:22 ([192.168.1.10]:22)' can't be established. ED25519 key fingerprint is SHA256:HLfnjG5dDqVPIfWtG6sNGG5JyY5AYtkxaupGsJffyYs. This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])?
yes
と答えれば接続が続行する。SSH のよくある使い方はサーバ上のシェルを遠隔実行するためのものだが、 SSH 層自体がいくつかのレイヤーから構成されており、 シェル以外の用途にも利用可能である:
ssh
コマンドは、自分のホームディレクトリ下にある
~/.ssh/config
という設定ファイルを利用する。
たとえば、以下のような内容:
Host myhost Port 222 (TCPポート番号) User aws-user-1 (ログインするユーザ名) IdentityFile ~/.ssh/id_ed25519_aws (認証に使う秘密鍵) Hostname myhost.something.us-east-1.servers.aws.com (実際のホスト名)
を書いておくと、以下のコマンド
client$ ssh myhost
を入力しただけで、 自動的に対象のホスト名・ユーザ・秘密鍵ファイルなどが指定される。
同様のことをコマンドラインで指定すると以下のようになる:
client$ ssh -p 222 -f ~/.ssh/id_ed25519_aws aws-user-1@myhost.something.us-east-1.servers.aws.com
SSHで認証に使う秘密鍵は、通常は暗号化されており、解凍するには毎回パスフレーズの入力が必要である。 秘密鍵保持エージェントを使うと、一度解凍した秘密鍵を一定時間保持しておくことができるため、 毎回パスフレーズを入力する必要がなくなる。
現在のほとんどのOSでは、秘密鍵保持エージェントは自動的に起動されている。
ここに秘密鍵を追加・削除するには、ssh-add
コマンドを使う。
ssh-add 秘密鍵ファイル
: 秘密鍵をエージェントに追加する。ssh-add -D
: エージェント上の秘密鍵をすべて削除する。ssh-add -l
: エージェント上の秘密鍵を一覧表示する。一般に、ネットワークにおける「セキュリティ」とは、以下の性質をさす:
先に述べた暗号化は上の性質 a., b. に対してある程度は有効だが、 完全ではない (そもそも「完全な」セキュリティなど現実にありえない)。 インターネットは本質的に信頼できないため、 暗号化以外にもネットワークからの攻撃を防ぐ仕組みがいくつも存在する。
そこで、以下のアドレスは組織内で勝手に使ってよいという 決まりになっている。これを プライベート IPアドレス といい、 プライベート IPアドレスを使ったネットワークを プライベート ネットワーク (private network) という。
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
これに対して、世界中から実際に到達可能な「普通の」IPアドレスのことを グローバル IPアドレスという。
アドレスの不足を解決するため、現在の IP 層を変更して 128ビットのアドレスを使えるようにしたものが IPv6 である。 (これに対して、従来の IP層は IPv4 という。) IPv6 を使えばすべての機器がグローバル IP アドレスを持つことができる。 しかし IPv6 は従来の IPv4 とは互換性がないため、 世界じゅうのルータを入れ換えなければならない。 このため、まだあまり普及していない。
具体的には、ルータが「どのプライベートIPアドレスがどのグローバルIPアドレスと 通信しているか」を覚えておき、それに該当する (双方向の) IPパケットが来るたびにそのアドレスを書き換えることによって実現する:
現在、ほとんどの家庭用ルータ・業務用ルータには NAT 機能が搭載されている。
プライベートネットワーク内のホスト 10.10.1.2
が
ゲートウェイ 10.10.1.1
を経由して、
インターネット上のホスト 55.66.77.88
と通信するとする。
このとき送信されるIPパケットの送信元・送信先アドレスを書け。
ファイヤウォール (Firewall) とは、ホストにとって 害のありそうなパケットを無視する (フィルタする) 機器またはソフトウェアのことである。 現在の一般的な環境では:
基本的にファイヤウォールは「外 → 内」のパケットをフィルタするが、 「内 → 外」のパケットをフィルタすることもある。
ファイヤウォールには、一般的に以下のようなルールが設定できる:
最近のネットワークでは、ファイヤウォールに加えて 以下のような機器も使われることが多い:
プロキシサーバ (proxy server) は、 プライベートネットワーク内にある PC が外部と通信するための方法のひとつである。 NAT とは異なり、IP層 (レイヤー3) ではなく TCP層 (レイヤー4) の接続を制御する。
GET / HTTP/1.1 Host: www.example.com …
Host:
を参照し、
あらためて宛先のWebサーバに向けてTCP接続して、同じリクエストを送る。
GET / HTTP/1.1 Host: www.example.com …
プライベートネットワークは、本来は外部からアクセスできない (ファイヤウォールで守られた) 安全な伝送路のみを使って構築するのが普通である。 しかし暗号化を使うことにより、安全でない伝送路を通って プライベートネットワークを構築することが可能になる。 これを VPN (Virtual Private Network、仮想プライベートネットワーク) とよぶ。
VPN では、別々の場所にある2つのサーバ間を暗号化通信でつなぎ、 仮想インターフェイスを使って 見かけ上2つのホストがあたかも同一ネットワーク上に配線されているかのように 通信する。
以下の図は、2つのサブネットを VPN 接続した例を表したものである。 赤い点線は、VPN による (見かけ上の) パケット通信を示している。
各ホストのルーティング規則は変更されており、
左側のホスト (192.168.1.0/24
) と
右側のホスト (192.168.2.0/24
) は
それぞれ VPN サーバをルータとして経由し、もう片方のホストにパケットを送信する。
また、双方のホスト名が解決できるように、DNSの設定も変更されることが多い。
実際に VPN を実現する手法にはさまざまなものがある:
一般に VPN を自分で構築・設定するのはかなり手間がかかる。 いっぽう SSH のポート転送機能を使うと、異なるネットワーク間における サーバの共有が簡単にできる。
ローカル→リモート転送を使うと、ローカル (クライアント側ネットワーク) から、 リモート (サーバ側ネットワーク) のサーバに TCP接続できる。
以下の構成は、リモートにあるサーバの TCP 80番にアクセスする例である。 この場合、クライアントPC上の SSHコマンド自身がプロキシサーバとして動作し、 8080番ポートに接続された TCP の通信をすべて相手サーバの 80番に転送する。
server$ nc -l 10000
client$ ssh -L 10000:localhost:10000 server.example.com
client$ nc localhost 10000
いっぽうリモート→ローカル転送を使うと、リモート (サーバ側ネットワーク) から ローカル (クライアント側ネットワーク) のサーバに TCP接続できる。
以下の構成は、ローカル内のサーバの TCP 80番にリモートからアクセスさせる例である。 この場合、リモートの SSHサーバがプロキシサーバとしても動作し、 ローカルの SSHクライアントと提携して、 8080番ポートに接続された TCP の通信をすべてローカルの 特定ホストの 80番に転送する。
client$ nc -l 10000
client$ ssh -R 10000:localhost:10000 server.example.com
server$ nc localhost 10000