ハッカソンで勉強がてらSTUN Clientを実装したのでメモ。
方針
STUNサーバのRFCは2本ある
(RFC5780もSTUNに関係しているけど、定義自体はこの2本)。このうちRFC5389はRFC3489をobsoleteしているので、実際には5389だけ読めばいい。
ただ変更記述を読んでも今ひとつピンとこなかったのと、ハッカソン中に実装が終わるかどうかということを考え、RFC3489のClientだけ実装することにした。
従って以下の記述はRFC3489に関するものである。
概要
NATの後ろに居るPrivate IPアドレスしか持っていないクライアントは、インターネット上の機器と通信することができないため、どこかでグローバルIPアドレスに変換してから通信する必要がある。これを担当する機器がNATである。
NATが変換する前のアドレスがInternal IP Address、NATが変換した後のIPアドレスがExternal IP Addressと呼ばれる。
External IP AddressはグローバルIPアドレスであるので、外部からデータを送りつけることが可能である。
うまい方法で送りつけることで、内部のクライアントまでデータを転送してくれれば、NATの向こう側の機器とP2Pで通信することができる。
さて、外部の機器へデータを送信して、返信する時、NATはどう動作するだろうか?
実は動作は1種類ではなく、実装に依存して変化する。
どのように動くのか把握することができれば、P2P通信を確立するための手法を検討する際に役に立つ。これを調べる方法として定義されているのがSTUNである。
RFC3489で定義されているNAT Typeは4つ。
- Full Cone
- 同一のinternal IPアドレス(*1)とポート番号から送信されるパケットは、常に同一のexternal IPアドレス(*2)とポート番号にマッピングされる
- 加えて、マッピングされたexternal IPアドレスとポート番号へ送信されたパケットは、送信元がいずれのexternal hostであっても、internal hostに転送される
- Restricted Cone
- 同一のinternal IPアドレスとポート番号から送信されたパケットは、常に同一のexternal IPアドレスとポート番号にマッピングされる
- Full Cone NATとは違って、internal hostが送信したパケットの宛先IPアドレスと同一のexternal IPアドレスからの通信のみinternal hostへと転送される
- Port Restricted Cone
- 同一のinternal IPアドレスとポート番号から送信されたパケットは、常に同一のexternal IPアドレスとポート番号にマッピングされる
- internal hostが送信したパケットの宛先IPアドレスとポート番号のペアから送信された通信のみinternal hostへと転送される
- Symmetric
- 同一のinternal IPアドレスとポート番号のペアから、同一のexternal IPアドレスとポート番号へと転送された全てのパケットが同一のexternal IPアドレスとポート番号にマッピングされる
- 通信を受信したexternal hostのIPアドレスとポート番号のペアからの通信のみがinternal hostへと転送される
以上のNATに加えて、「Open Internetに接続されている」「NATの下にいないけどFirewallで遮断されている」という状況を想定し、現在のネットワークがどれに同等するのかを判断することを目的としている。
STUN ClientがSTUN Serverにメッセージを送信し、それに対してサーバがいろいろな情報を載せて返すことで判断を行う。
Clientから送るメッセージがBinding Resuest、Serverから返すメッセージがBinding Responseと呼ばれる。Binding RequestとBinding Responseの区別はSTUN Messageのヘッダで行う。Binding RequestとBinding Responseは様々な情報を伝えるため、Attributeとして定義された情報を持つことができる。
要するに以下のコードの用に定義できる。
pub struct StunMessage {
pub header: StunHeader,
pub attributes: Vec<Attribute>,
}
pub struct StunHeader {
message_type: u16,
length: u16,
transaction_id: [u8; 16],
}
message_typeでBinding RequestとBinding Responseを区別する。
定義されているtypeは以下の通り。
0x0001 : Binding Request
0x0101 : Binding Response
0x0111 : Binding Error Response
0x0002 : Shared Secret Request
0x0102 : Shared Secret Response
0x0112 : Shared Secret Error Response
Attributeは任意の個数設定できるため、パケットが可変長であり、長さを知るためlengthのフィールドがある。Headerは20バイトで固定なので、lengthの数値には含まない。
Transaction IDはBinding ResponseがどのBinding Requestに対する応答なのかを特定するための識別子である。128bitあり十分長くまず重複しないので、送信側がランダム決めてよい。
さて実際にどのように判断を行うのか。RFC3489には3回テストして判断する例が載っているのでこれを追いかけることにする。
+--------+
| Test |
| I |
+--------+
|
|
V
/\ /\
N / \ Y / \ Y +--------+
UDP <-------/Resp\--------->/ IP \------------->| Test |
Blocked \ ? / \Same/ | II |
\ / \? / +--------+
\/ \/ |
| N |
| V
V /\
+--------+ Sym. N / \
| Test | UDP <---/Resp\
| II | Firewall \ ? /
+--------+ \ /
| \/
V |Y
/\ /\ |
Symmetric N / \ +--------+ N / \ V
NAT <--- / IP \<-----| Test |<--- /Resp\ Open
\Same/ | I | \ ? / Internet
\? / +--------+ \ /
\/ \/
| |Y
| |
| V
| Full
| Cone
V /\
+--------+ / \ Y
| Test |------>/Resp\---->Restricted
| III | \ ? /
+--------+ \ /
\/
|N
| Port
+------>Restricted
(https://www.ietf.org/rfc/rfc3489.txt Figure2を引用)
テストの流れ
テスト1
まずテスト1ではフラグを立てずにBinding Requestを送ってみる(フラグについては後述)。応答が帰ってこなければそもそも通信ができていないので、UDP Blockedと判定して終了する。
応答(Binding Response)が帰ってきた場合、Binding Responseの含むAttributeを見て判定を行う。まずこの時点で知りたいのは、NATの背後にいるかどうかである。
NATの背後にいる場合、STUN Serverへ届くパケットのIPアドレスはNATが割り当てたものに変わっている。これをSTUN Serverから送り返してもらって、自分のIPアドレスと比較できるとよい。
このためにMAPPED-ADDRESS Attributeが定義されている。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|x x x x x x x x| Family | Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
コードだとこんな感じ
pub struct MappedAddress {
pub family: u8,
pub port: u16,
pub address: Ipv4Addr,
}
ちなみにSTUN 3489ではfamilyというフィールドは常に0x01であり、IPv4を表す。IPv6やその他のアドレスファミリーでは利用できない(なのでRustのコードはIpv4Addrで実装した)。
Binding ResponseのMAPPED-ADDRESS Attributeを見ることで、NATの後ろに居るかどうかが分かる。
一致していればアドレスは変換されていないからNATの後ろではないし、一致しなければ変換されているのでNATの後ろである。
ここで受け取ったMAPPED-ADDRESSは、後で使うので残しておく(*1)
テスト2
Stun ClientがNATの後ろに居るかどうかの判断がテスト1でできた。
次は挟まっているNATの種類の判定を始める。また、NATが挟まっていない場合であっても、通信がFirewallによって阻害される可能性があるかどうかは知りたいところであるので、この判定も行う。
テスト2ではNATが無い場合はFirewallで通信遮断されるかどうか、NATがある場合はFull Cone NATかどうかの判定を行う。
Full Cone NATは、
マッピングされたexternal IPアドレスとポート番号へ送信されたパケットは、送信
元がいずれのexternal hostであっても、internal hostに転送される
ということであった。要するにStun ClientがStun Serverに送ったメッセージの宛先IPアドレスとポート番号とは完全に別のところから送られた通信を受信できるかどうかを試せば良い。このことをStun Serverに伝えるため、CHANGE-REQUESTというAttributeにフラグを立ててStun Serverに送信する。
CHANGE-REQUESTは以下の通りに定義されており、フラグを2つ含む。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 A B 0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
実装はこうした
pub struct ChangeRequest {
a: bool,
b: bool,
}
フラグAは"change IP"フラグである。falseの場合はStun ServerはBinding Requestを受け取ったIPアドレスから返信すれば良い。trueの場合は別のIPアドレスから返信することになる。
フラグBは"change Port"フラグである。falseの場合はStun ServerはBinding Requestを受け取ったポートから返信すればよい。trueの場合は別のポート番号から返信することになる。
さてテスト2では、change IP, change Port両方のフラグを立てたCHANGE-REQUEST Attributeを含んでStun ServerへBinding Requestを送信する。
Stun Serverはパケットを受け取ったのとは異なるIPアドレスとポート番号からBinding Responseを送る。
IPアドレスとポート番号両方が異なるので、FirewallやNATにとっては、戻りの通信は送信パケットとは全く関係ないものに見える。これが通るかどうかというテストである。
NATが無い場合Firewallで遮断すらされていなくて、完全にOpenなインターネットに直結されているということになる。要するにFirewallが設定されていないサーバのような状態である。
通らなければFirewallで遮断されているということになる。
NATがある場合、これが通ればFull Cone NATの後ろであると判断することができる。通らなければFull Cone NATではない残り3タイプのどれかである。
以降ではこのStun Clientの前に居るNATが残り3タイプのどれかの判定を行う。
テスト1をもう一回
ここまでの検証により、どこから転送されたパケットでも受け取れるというFull Cone NATではないことが分かった。
次にSymmetric NATかどうかの判断を行う。
Symmetric NATは、Stun Clientの送信元IPアドレスとポート番号が同じであっても、毎回マッピングされるIPアドレスとポート番号が異なるNATである。
要するにStun ServerへBinding Requestをもう一回送って、帰ってきたMAPPED-ADDRESSが前回取得したもの(*1)と同じかどうか見れば良い。
ここではBinding Responseを確実に受け取るため、change IP, change Portフラグはfalseにして送信する。要するにテスト1と全く同じことをもう一回する。テスト1でBinding Responseが受け取れたという実績があるので、上流で変更や障害がなければ、まず間違いなく受信することができる。
以上2回のテスト1で得られたMAPPED-ADDRESSを比較して、一致して居なければSymmetric NATの背後に居るということが分かる。受信できた場合はRestricted NATかPort Restricted NATのどちらかである。これはテスト3で判断する。
テスト3
テスト3では、Binding Request中のCHANGE-REQUESTに、change Portフラグのみtrueにして送信する。
これに対して返信されたBinding Responseが受信できれば、IPアドレスさえ同じであればポート番号が変わっていても受け取れるということになる。つまりRestricted NATである。
Binding Responseが受け取れなかった場合は、IPアドレスもポート番号も一致しないと返信が受け取れないということになる。つまりPort Restricted NATである。
その他の要素
3489 STUNの内容は基本的に以上である。しかしRFCを読むとAttributeは他にも沢山定義されている。
0x0001: MAPPED-ADDRESS
0x0002: RESPONSE-ADDRESS
0x0003: CHANGE-REQUEST
0x0004: SOURCE-ADDRESS
0x0005: CHANGED-ADDRESS
0x0006: USERNAME
0x0007: PASSWORD
0x0008: MESSAGE-INTEGRITY
0x0009: ERROR-CODE
0x000a: UNKNOWN-ATTRIBUTES
0x000b: REFLECTED-FROM
残りの要素は基本的に攻撃を食らった時上手く動くことを保証するためのものである。
RESPONSE-ADDRESS
RESPONSE-ADDRESSはBinding Requestに含めても良い(CAN)もので、Binding Responseの送信先を示す。このAttributeがある場合、STUNサーバはAttributeの情報に従ってBinding Responseを返す。
このAttributeが無い場合は、パケットの送信元IPアドレスとポート番号に対して通信を行う。
IPアドレスとポート番号が分かれば良いので、フォーマットはMAPPED-ADDRESSと同じである。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|x x x x x x x x| Family | Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
pub struct ResponseAddress {
family: u8,
port: u16,
address: Ipv4Addr,
}
CHANGED-ADDRESS
Binding Responseに常に含まれるもので、Binding RequestのCHANGE-REQUESTでchange IPやchange Portのフラグが設定されていた場合、変更されたIPアドレスとポート番号を示す。(変更されない場合はこのAttributeは含まれない)
IPアドレスとポート番号が分かれば良いので、フォーマットはMAPPED-ADDRESSと同じである。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|x x x x x x x x| Family | Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
pub struct ChangedAddress {
family: u8,
port: u16,
address: Ipv4Addr,
}
SOURCE-ADDRESS
SOURCE-ADDRESSはBinding Responseに含まれるAttributeである。
Binding Responseの送信元IPアドレスとポート番号を示す。
IPアドレスとポート番号が分かれば良いので、フォーマットはMAPPED-ADDRESSと同じである。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|x x x x x x x x| Family | Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
pub struct SourceAddress {
family: u8,
port: u16,
address: Ipv4Addr,
}
USERNAME, PASSWORD, MESSAGE-INTEGRITY
これら3つのAttributeはメッセージの完全性を目的として利用される。
これらの値が不正であれば、Binding Error Responseを返す。
ERROR-CODE
ERROR-CODE Attributeは何らかのエラーが発生したことを示す。
エラーコードと理由を示すフレーズを含む。
UNKNOWN-ATTRIBUTE
UNKNOWN-ATTRIBUTEはBinding Error ResponseかShared Secret Error ResponseでERROR-CODE 420の際に含まれるものである。
Binding Requestの中の必須Attributeが理解できなかった時に返される。理解できなかったAttribute Typeの列である。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Attribute 1 Type | Attribute 2 Type |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Attribute 3 Type | Attribute 4 Type ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+