L4 Echo Server作りながらRustを嗜む

はじめに

本年もよろしくお願いいたします🎍

毎年、年末には何かしら新しい学びをインプットする時間を作っているのだが、
今年は諸々の事情で年内に間に合わず、年始から勉強を始めることに!

仕事上Rustに触る機会がなく、明確に「これを作りたい」というテーマもなかったのだが、
せっかくなので「低レイヤーで何か作りたい」という気持ちを優先することにしました。

そこで以前から一度やってみたかった、TCPの3ウェイハンドシェイクをRaw Socketで実装することに挑戦してみた。

HTTPなどのアプリケーション層のプロトコルは一切使わず、
トランスポート層(L4)で直接通信し、文字列を返すだけのシンプルなサーバーを作る。
いわゆる「エコーサーバー」をL4だけで完結させるというのが今回のテーマです。

完成形はこれ:

1
2
$ echo "toppage" | nc 172.17.0.2 8888
hello world

シンプルだけど、これを実現するまでにTCPの仕組みを体系的に理解できた。
一点注意としては、本件は実用性は全く意識しておらず、ハードコーディングのオンパレードです。

何を作ったか

L4 String Server - トランスポート層で文字列を返すだけのサーバー

  • Raw Socketで実装
  • TCP 3ウェイハンドシェイク(SYN, SYN-ACK, ACK)
  • データパケットの送受信

使用技術:

  • Rust (初めて)
  • Raw Socket (libcでシステムコール呼び出し)
  • Docker (macOSのloopback制限回避)
  • Claude Code(Agentic codingで学びを加速🙇)

作った経緯

OSI参照モデル見るたびに「アプリケーション層を通さずに3ウェイハンドシェイクの検証ってできないんだっけ?」とずっと思った時期があったので、実験的な意味も含めてやってみたかった。

よく見るこの図の赤枠部分だけでデータやり取りするという試み。
この辺は先人の方々がすでにやられているがあくまでRustの理解を深めるというモチベーションで頑張る。

いざ開発

1. IPヘッダーの解析

IPヘッダーの仕様理解(RFC 791)と受信したパケットからIPヘッダーをパースする。

IPヘッダーの構成

このアスキーアートはAIで出力。
難しい。

1
2
3
4
5
6
7
8
9
10
11
12
13
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length | ← 0-3バイト
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset | ← 4-7バイト
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum | ← 8-11バイト
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address | ← 12-15バイト
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address | ← 16-19バイト
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
1
2
3
4
5
6
7
8
9
10
11
12
13
| バイト位置 | フィールド         | 説明                      |
|------------|--------------------|---------------------------|
| 0 | Version (4bit) | IPバージョン (4 = IPv4) |
| | IHL (4bit) | ヘッダー長 (5 = 20バイト) |
| 1 | Type of Service | サービスタイプ |
| 2-3 | Total Length | パケット全体の長さ |
| 4-5 | Identification | パケット識別子 |
| 6-7 | Flags + Fragment | フラグメント情報 |
| 8 | Time to Live (TTL) | 生存時間 |
| 9 | Protocol | プロトコル (6=TCP) |
| 10-11 | Header Checksum | チェックサム |
| 12-15 | Source IP | 送信元IPアドレス |
| 16-19 | Destination IP | 宛先IPアドレス |

受信したパケットからIPヘッダーをパース

重量なのは送信元IPアドレス、宛先IPアドレス

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct IpHeader {
version: u8,
ihl: u8,
src_ip: [u8; 4],
dst_ip: [u8; 4],
}

impl IpHeader {
fn parse(data: &[u8]) -> Option<Self> {
if data.len() < 20 {
return None;
}

let version_ihl = data[0];
let version = version_ihl >> 4; // 上位4ビット
let ihl = version_ihl & 0x0F; // 下位4ビット

Some(IpHeader {
version,
ihl,
src_ip: [data[12], data[13], data[14], data[15]],
dst_ip: [data[16], data[17], data[18], data[19]],
})
}
}

ビット演算でバージョンとヘッダー長を取り出す。

2. TCPヘッダー解析

TCPヘッダーの理解(RFC 793)とパース。

TCP ヘッダーの構成

1
2
3
4
5
6
7
8
9
10
11
12
13
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port | ← 0-3バイト
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number | ← 4-7バイト
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number | ← 8-11バイト
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window | ← 12-15バイト
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
1
2
3
4
5
6
7
8
9
10
11
| バイト位置 | フィールド            | 説明                     |
|------------|-----------------------|--------------------------|
| 0-1 | Source Port | 送信元ポート番号 |
| 2-3 | Destination Port | 宛先ポート番号 |
| 4-7 | Sequence Number | シーケンス番号 |
| 8-11 | Acknowledgment Number | ACK番号 |
| 12 | Data Offset | ヘッダー長 |
| 13 | Flags | SYN, ACK, FIN等 ← 重要! |
| 14-15 | Window | ウィンドウサイズ |
| 16-17 | Checksum | チェックサム |
| 18-19 | Urgent Pointer | 緊急ポインタ |

TCP ヘッダーのパース

1
2
3
4
5
6
7
struct TcpHeader {
src_port: u16,
dst_port: u16,
seq_num: u32,
ack_num: u32,
flags: u8,
}

フラグ判定が重要

1
2
3
4
5
6
7
8
const TCP_FIN: u8 = 0x01; // もう送るデータはありません
const TCP_SYN: u8 = 0x02; // 接続開始要求
const TCP_PSH: u8 = 0x08; // バッファに溜めず、すぐアプリに渡してほしい
const TCP_ACK: u8 = 0x10; // 受信確認

fn is_syn(&self) -> bool {
self.flags & TCP_SYN != 0
}

3. SYN-ACKパケット作成

ここが一番ハマった。

問題1: カーネルがRSTを送ってくる

SYNを受信してSYN-ACKを返そうとすると、カーネルが勝手にRSTパケットを送信してしまう。カーネルから見ると「知らないポートへのSYN」なので、RSTで拒否するのは正しい動作。

解決: iptablesでRSTをブロック

これでカーネルのRSTを破棄できた。
実用性皆無。

1
iptables -A OUTPUT -p tcp --tcp-flags RST RST --sport 8888 -j DROP

問題2: SYN-ACKがeth0に出る

SYNはloインターフェースから来るのに、SYN-ACKがeth0から出て行ってしまう。

解決: 送信用ソケットをloにバインド

受信用と送信用でソケットを分けることで解決。
これまた実用性皆無。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsafe fn create_send_socket() -> Result<c_int, String> {
let sock = libc::socket(AF_INET, SOCK_RAW, IPPROTO_TCP);

// loインターフェースにバインド
let interface_name = b"lo\0";
libc::setsockopt(
sock,
libc::SOL_SOCKET,
libc::SO_BINDTODEVICE,
interface_name.as_ptr() as *const c_void,
interface_name.len() as u32,
);

Ok(sock)
}

4. チェックサム計算

IPとTCPのチェックサムを計算する必要がある。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn calculate_checksum(data: &[u8]) -> u16 {
let mut sum: u32 = 0;
let mut i = 0;

// 2バイトずつ足していく
while i < data.len() - 1 {
let word = u16::from_be_bytes([data[i], data[i + 1]]);
sum += word as u32;
i += 2;
}

// 桁上がりを折り返す
while sum >> 16 != 0 {
sum = (sum & 0xFFFF) + (sum >> 16);
}

// 1の補数
!sum as u16
}

TCPは疑似ヘッダーを含めてチェックサムを計算する:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn calculate_tcp_checksum(src_ip: &[u8; 4], dst_ip: &[u8; 4], tcp_segment: &[u8]) -> u16 {
let tcp_len = tcp_segment.len() as u16;

let mut pseudo = Vec::new();
pseudo.extend_from_slice(src_ip);
pseudo.extend_from_slice(dst_ip);
pseudo.push(0);
pseudo.push(6); // Protocol = TCP
pseudo.extend_from_slice(&tcp_len.to_be_bytes());
pseudo.extend_from_slice(tcp_segment);

calculate_checksum(&pseudo)
}

5. データパケット送受信

コマンドを受け取って、レスポンスを返す部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// コマンド受信
let data = &buffer[data_offset..recv_len as usize];
let command = String::from_utf8_lossy(data).trim().to_string();

// レスポンス生成
let response = match command.as_str() {
"toppage" => "hello world\n",
"about" => "This is TCP server\n",
"help" => "Available commands: toppage, about, help\n",
_ => "Unknown command\n",
};

// データパケット送信
let data_packet = build_data_packet(
my_server_ip,
conn.client_ip,
8888,
conn.client_port,
conn.server_seq,
conn.client_seq,
response.as_bytes(),
);

6. FIN処理 (最後の仕上げ)

最初はncが終了しなかった。

問題: ncが終了を待っている

データを受信しても、ncは接続を閉じずに待機していた。

1
2
3
4
1. nc → サーバー: "toppage" (PSH+ACK)
2. サーバー → nc: "hello world" (PSH+ACK)
3. nc → サーバー: ACK
4. 【ここで待機】← ncがサーバーの切断を待っている

解決: サーバーからFIN-ACKを送る

データレスポンス送信直後に、サーバーからFIN-ACKを送ることで解決。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// データ送信
send_packet(send_sock, &data_packet, conn.client_ip)?;

// 即座にFIN-ACK送信
let fin_ack = build_fin_ack_packet(
my_server_ip,
conn.client_ip,
8888,
conn.client_port,
conn.server_seq,
conn.client_seq,
);

send_packet(send_sock, &fin_ack, conn.client_ip)?;

これでncが自動終了するようになった!

1
2
$ echo "toppage" | nc 172.17.0.2 8888
hello world
入力のパケットをWiresharkで確認
出力のパケットをWiresharkで確認

最終的なパケットフロー

%%{init: {'theme':'forest'}}%%
sequenceDiagram
    participant Client as クライアント(nc)
    participant Kernel as Linuxカーネル(TCPスタック)
    participant Program as プログラム(Raw Socket)

    Note over Client,Program: ① 3ウェイハンドシェイク

    Client->>Kernel: SYN (lo)
    Kernel->>Program: SYN
    Note right of Program: 受信

    Kernel--xClient: RST (blocked!)
    Note left of Kernel: iptablesで破棄

    Note right of Program: SYN-ACK作成
    Program->>Client: SYN-ACK (lo)
    Note right of Program: 送信

    Client->>Kernel: ACK (lo)
    Kernel->>Program: ACK
    Note right of Program: 受信
Established Note over Client,Program: ② データ交換 Client->>Kernel: PSH+ACK ("toppage") (lo) Kernel->>Program: PSH+ACK ("toppage") Note right of Program: 受信
コマンド解析 Program->>Client: PSH+ACK ("hello world") (lo) Note right of Program: 送信 Client->>Kernel: ACK (lo) Kernel->>Program: ACK Note right of Program: 受信 Note over Client,Program: ③ 接続クローズ Program->>Client: FIN+ACK (lo) Note right of Program: 送信
接続リセット Client->>Kernel: FIN+ACK (lo) Kernel->>Program: FIN+ACK Note right of Program: (無視) Note left of Client: 終了 Note over Kernel: 無視 Note right of Program: 次の接続待機

学んだこと

Rustの基本的な構文(構造体、パターンマッチ、Option<T>、テスト)は触ってて楽しかった。

TCPの3ウェイハンドシェイクは理論では知ってたけど、実装してみるとシーケンス番号とACK番号の管理が思ったより重要だった。チェックサム計算も疑似ヘッダー含めて計算する必要があって、細かい仕様が多い。

ハマったポイントは:

  • macOSのloopback制限でDocker使った
  • カーネルが勝手にRST送ってくるのをiptablesでブロック(実用性皆無)
  • 送信パケットがeth0に出ちゃうのを送信用ソケット分離で解決(これも実用性皆無)
  • ncが終了しない問題はサーバーからFIN送って解決

おわりに

最初は「SYNパケット受信してSYN-ACK返すだけでしょ?」って思ってたけど、予想外に面倒だった。でもWiresharkでパケット見ながら「ちゃんと動いてる!」ってなるのは楽しい。

ということでRustを学んでいきました〜
仕事でも活用していきたい〜

今回使ったコードは以下
https://github.com/kurum-inc/rust-l4-echo-server

GitHub Actions で OIDC を使用して AWS 認証ってどういう仕組みなん?

いやー久しぶりってレベルじゃないぐらいのレベルでブログ書いてます。

みなさんはGithub Actionsを使って、AWSにデプロイしたり、ファイルをアップロードしたりってしますかね?

その際にこんなこと思ったことありませんか?

これどういう仕組みでAWSとやりとりしてんだろ?

GitHub Actionsではロール名だけ教えればAWSと連携できちゃうなんて、そんなに簡単に認証できていいわけ!?

このように思ったのは私だけ?

というわけで、GitHub Actions で OIDC を使用したAWS認証の仕組みの謎を追っていきたいと思います🧭


ちょっとその前に

そもそもOIDCってなんだっけ?

OIDC(OpenID Connect)は OAuth 2.0 を拡張した仕様

OAuth 2.0は認可の仕組みで、認証に使われちゃったりする問題(認可情報を認証としてつかってしまう問題)がでてきたので、ちゃんと認証の仕組みいれようということで出てきた仕組み。(雑な説明ですいません)

OAuth 2.0がアクセストークンで認可をするのに対して、OIDCではIDトークンで認証を行う。

参考:
https://www.sakimura.org/2012/02/1487/

AWS AssumeRole(アシュームロール)ってよく聞くどあんま理解できてない

AssumeRole(アシュームロール)は簡単にいうと「一時的に他の役割を借りる」ことです。
特定の権限を持つIAMロールを引き受けることができます。

例えば、GitHub ActionsがAWSのS3とかLambdaとかにアクセスする権利が一時的に与えられます。

STSもよくきけどなんなん?

STSもよく聞くけど、これは「AWS Security Token Service」の略で、AWSのサービスの一つです。
STSは一時的なセキュリティ認証情報(トークン)を生成するために使用されます。これにより、ユーザーやアプリケーションは一時的なアクセス権を持つことができ、長期的なアクセスキーを使用する必要がなくなります。

OIDCトークンというものがある

OIDCトークンはJSON Web Token (JWT) 形式で発行されている。

今回の場合はGithub Actionsから払い出されるトークンで、ジョブ実行時の情報やレポジトリの情報も含まれる。

GithubActionsのOIDCトークンの仕様は以下
https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token


事前設定

1. AWS: Github Actionsのフィンガープリントを設定

フィンガープリント(TLS証明書の一意なハッシュ値)を事前に設定することで、AWSはGitHub以外の第三者が偽の公開鍵を提供しても、それが正当な公開鍵でないことを判別できます。これにより、公開鍵のなりすましを防ぎます。

公開鍵は後のOIDCトークンの署名(Github Actionsが署名したOIDCトークンかをチェック)に利用。

フィンガープリントの設定不要になってました。
https://github.com/aws-actions/configure-aws-credentials/issues/357#issuecomment-1626357333

信頼性の高いCAライブラリで通信を保護してるから改善されないってことでしょうかね。

2. AWS: クライアント IDの設定

AWS側で「クライアントID」を設定することで、GitHub ActionsからAWSに送られてくるトークンが、AWSで利用されることを確認します。
具体的には、GitHub Actionsが発行するOIDCトークンの「aud」(オーディエンス)クレームに「sts.amazonaws.com」というクライアントIDが含まれているかをAWSでチェックします。

3. AWS: Githubのレポジトリーを設定

GitHub ActionsがAWSリソースへアクセスできるようにするには、AWS側でどのGitHubリポジトリを信頼するか設定します。これにより、特定のリポジトリだけがAWSのリソースにアクセスできるように制限できます。

OIDCトークンに含まれるレポジトリ名が一致するか検証するのに利用

1
2
3
4
5
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref/heads/main"
}
}

4. Github Actions用のロール作成

GitHub ActionsがAWSリソースにアクセスするためのIAMロールを作成します。このロールには、GitHub Actionsが利用する特定のリソースや操作に対するアクセス権限を設定します。

例)S3バケットへの読み書き権限をあたえるなど


Github Actionsのジョブ実行

それでは、おおまかな流れを見ていきましょう!

%%{init: {'theme':'forest', 'primaryColor': '#ff0000'}}%%
sequenceDiagram
    participant GitHub_Actions as GitHub Actions
    participant AWS as AWS
    participant AWS_Secrets as AWS STS

    GitHub_Actions->>GitHub_Actions: 1. OIDCトークンの署名\n(ヘッダー, ペイロード, 署名)
    GitHub_Actions->>AWS: 2. OIDCトークンの送信\n(ヘッダー, ペイロード, 署名)

    AWS->>GitHub_Actions: 3. 信頼性の高いCAライブラリで安全に公開鍵を取得

    AWS->>AWS: 4. OIDCトークンの署名の検証\n(Base64URLデコードして確認)
    AWS->>AWS: 5. トークンの検証\n(発行者, 受信者, 有効期限)

    AWS->>AWS_Secrets: 6. 一時的な認証情報の発行\n(IAMロールを引き受け)
    AWS_Secrets-->>GitHub_Actions: アクセスキー, シークレットアクセスキー, セッショントークン

    GitHub_Actions->>AWS: 7. 一時的な認証情報の使用\n(AWSリソースにアクセス)

1. GitHub Actions: OIDCトークンの署名

GitHub Actionsは、ジョブの実行中にOIDCトークンを取得します。このトークンはJWT形式で、ヘッダー、ペイロード、署名の3つの部分から構成されています。
GitHubは、トークンのヘッダーとペイロードを自身の秘密鍵で署名(ハッシュ化)して、署名部分を作成します。

2. GitHub Actions: OIDCトークンの送信

GitHub Actionsは、作成したOIDCトークン(ヘッダー、ペイロード、署名)をAWSに送信します。

3. AWS: 公開鍵の取得

AWSは、事前に設定されたTLS証明書のフィンガープリントを使用して、GitHubの公開鍵を取得します。この公開鍵は、GitHubのOIDCエンドポイントから取得したTLS証明書に含まれています。

信頼性の高いCAライブラリでtokens.actions.githubusercontent.comから安全に公開鍵を取得。

4. AWS: 署名の検証

AWSは、受信したOIDCトークンのヘッダーとペイロードをBase64URLデコードします。
AWSは、GitHubの公開鍵を使用して、トークンの署名を検証します。具体的には、トークンのヘッダーとペイロードを再度ハッシュ化し、その結果がトークンの署名部分と一致するかどうかを確認します。

[個人的重要ポイント]

公開鍵で検証できるのは、**GitHubしか秘密鍵を持っていないからこそ可能**な仕組みです。

5. AWS: トークンの検証

署名が一致する場合、AWSはトークンが改ざんされていないことを確認します。
AWSは、トークンのペイロードに含まれるクレーム(例:発行者、受信者、有効期限など)を検証します。

AWSは、トークンのaudクレームが「sts.amazonaws.com」であることを確認し、それがAWS用のトークンであることを認識します。また、token.actions.githubusercontent.com:subが事前に設定したリポジトリーと一致するかを確認し、許可されたリポジトリーからのトークンであることをチェックします。

6. AWS: 一時的な認証情報の発行

トークンが有効であることが確認された場合、AWS STSは指定されたIAMロールを引き受けるための一時的な認証情報(アクセスキー、シークレットアクセスキー、セッショントークン)を発行します。

7. GitHub Actions: 一時的な認証情報の使用

GitHub Actionsは、取得した一時的な認証情報を使用して、指定されたAWSリソースにアクセスします。

という感じでOIDCの仕組みを使ってうまいことやりとりをしていましたというお話でした。

では👋

参考サイト

NATを越えろ

自宅のデスク周りを充実させまくったら、部屋から完全に出不精になってしまいました。

ビデオ会議をする機会が大分増えてきて、今後もビデオチャットで仕事をすることや、プライベートでも活用の幅がふえてくるのでは?ということでせっかくなので※1 WebRTCでビデオチャットを実装した際にNATについて色々学びがあったのでここでアウトプット。

※1. Web RealTime Communicationの略でブラウザで気軽にリP2P通信ができるようにしたもの(オープン標準技術)

以下WebRTCのプロトコルスタック
主に赤枠の部分についての話

図:オライリー

そもそもNATってなんやねん

NAT(Network Address Translation)とは、コンピュータネットワークにおいて、主にグローバルIPアドレスをプライベートIPアドレスに、またはその逆の変換を行う技術のことである。

by wikipedia

つまりプライベートアドレスをグローバルアドレスに変換してインターネット上のアドレスとやりとり出来るようになる

ローカルネットワークとグローバルネットワークを行き来するための仕組み

静的・動的?

静的NAT

グローバルアドレスとプライベートアドレスを1対1の固定ペアとして変換を行う

動的NAT

外部への通信が発生したときに、プール内のグローバルアドレスを使用して変換を行う

NAPT(動的)

NAPT(Network Address Port Translation)とは、アドレスに加えてポート番号も変換する技術。
グローバルアドレスがひとつの場合でも、動的に割り当てられた送り元Portと送り先Portを元に関連付けを行うことができる。

家や会社で一つのグローバルIPで同時にインターネットができるのはこの仕組みのおかげ

送り元IP:送り元ポート.送り先IP:送り先ポート内容で接続ごとに管理している

NATタイプ

Full cone NAT(フルコーンNAT)

送信したことがないアドレスでも受信可能
※UPnPのポートマッピングはほぼこれに該当する

Address-Restricted cone NAT(制限付きコーンNAT)

一度送信した端末(NATデバイス上にWAN側に送信先アドレスのエントリが存在する場合)であれば受信可能

Port-Restricted cone NAT(ポート制限付きコーンNAT)

一度送信した端末(NATデバイス上にWAN側に送信先アドレスと送り元Portのエントリが存在する)であれば受信可能

Symmetric NAT(対象型NAT)

対象型NATは最も制限の厳しいもので、宛先アドレスとポートを用いて接続先を区別し、一つの接続先に対し一つのアドレス変換を用いる

Nat traversal(NAT越えの手法)

以上をふまえると(ふまえなくても)NATを越えるのがいかに難しく、不確定要素が多いのかが分かります。。。

人様のPCに外部から接続なんで冷静に考えたら厳しいのは当たり前ではあるのですが(; ・`д・´)

NATを越えるためにいくつか手法として、ルーターで対応してるもの(UPnP)や、connection reversalなどがありますが、
WebRTCで利用されるSTUNという仕様があり、その中で利用されているUDP hole punchingについてちょっと触れてみた。

UDP hole punching

上記Cone型による接続を行う際にファイヤーウォールとかどうやって突破すんねんって疑問が沸くかと思いますが、
それをうまいことするのがこのUDP hole punching。

上記の仕組みを使うことで、NAT外サーバー(STUN SERVER、SIGNALING SERVER)を経由してアドレス交換を行い、NAT内ホスト同士をNAT外から通信の開始を可能にしている。

まとめ

今回は触れませんでしたが、Peer間で最適な通信方法(STUN, TURN)の交渉にICEという仕組みがあったりとか、
STUNで越えられない場合の最終手段TURNなども今回ビデオチャットを実装する上で軽く触れましたが、割愛させていただきます(*´з`)

DNSのデバックのおともにdigコマンド

DNSサーバーの動作確認をする際のコマンドとして、nslookupとか使って対話的に確認したりすると思うけど、
どうやらUNIX, Linux的に非推奨だということが(今更)分かり、digコマンドをようやく使ってみたメモです。

また、nslooupより高機能らしいので高機能好きの僕には今が覚える好機。(いみふ)

ちなみにwindowsには標準ではいってないので使う方は頑張ってインストールしてください。

まずはシンプルに実行


実行コマンド
1
$ dig google.com
実行結果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[admin@localhost ~]$ dig google.com

; <<>> DiG 9.9.4-RedHat-9.9.4-73.el7_6 <<>> google.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17627
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;google.com. IN A

;; ANSWER SECTION:
google.com. 299 IN A 216.58.197.142

;; Query time: 54 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: 10 22 17:45:30 JST 2019
;; MSG SIZE rcvd: 55

確かにnslookupより情報量が多い( ゚Д゚)

status 応答コード
NOERROR 正常
REFUSED 拒否
flags 応答コード
qr サーバーからの回答が正常に行われた
rd 再帰的問い合わせを要求
ra サーバが再帰的問い合わせをサポート

※ここでいう再起はプログラミング的な再起処理ではなくて、「再び帰ってくる」という意味でちゃんとした結果を返してくることをサポートしますよ!ということ。

1
2
3
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17627
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

Question

オプションに何も指定せずに実行するとクエリタイプが「a」になるっぽい

1
2
;; QUESTION SECTION:
;google.com. IN A

Answer

Aレコードの内容がきちんと見れました

1
2
;; ANSWER SECTION:
google.com. 299 IN A 216.58.197.142

リソースを指定したい場合


■ ns(ネームサーバー)

実行コマンド

1
$ dig ns google.com

実行結果

Answerセクション部分のみ抜粋

1
2
3
4
5
;; ANSWER SECTION:
google.com. 19288 IN NS ns3.google.com.
google.com. 19288 IN NS ns1.google.com.
google.com. 19288 IN NS ns4.google.com.
google.com. 19288 IN NS ns2.google.com.

■ mx(メール交換情報)

実行コマンド

1
$ dig ms google.com

実行結果

Answerセクション部分のみ抜粋

1
2
;; ANSWER SECTION:
google.com. 234 IN A 172.217.161.46

■ any

実行コマンド

1
$ dig any google.com

実行結果

全部入り的なやつ

Answerセクション部分のみ抜粋

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;; ANSWER SECTION:
google.com. 299 IN A 216.58.197.142
google.com. 299 IN AAAA 2404:6800:4004:800::200e
google.com. 21599 IN NS ns1.google.com.
google.com. 599 IN MX 30 alt2.aspmx.l.google.com.
google.com. 599 IN MX 10 aspmx.l.google.com.
google.com. 599 IN MX 20 alt1.aspmx.l.google.com.
google.com. 299 IN TXT "docusign=1b0a6754-49b1-4db5-8540-d2c12664b289"
google.com. 21599 IN NS ns2.google.com.
google.com. 599 IN MX 50 alt4.aspmx.l.google.com.
google.com. 21599 IN NS ns3.google.com.
google.com. 599 IN MX 40 alt3.aspmx.l.google.com.
google.com. 3599 IN TXT "globalsign-smime-dv=CDYX+XFHUw2wml6/Gb8+59BsH31KzUr6c1l2BPvqKX8="
google.com. 59 IN SOA ns1.google.com. dns-admin.google.com. 275806207 900 900 1800 60
google.com. 3599 IN TXT "v=spf1 include:_spf.google.com ~all"
google.com. 21599 IN NS ns4.google.com.
google.com. 3599 IN TXT "facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95"
google.com. 21599 IN CAA 0 issue "pki.goog"
google.com. 299 IN TXT "docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e"

ネームサーバーを指定する場合

ネームサーバー切り替え前にデバックするときに重宝する

実行コマンド

1
$ dig a google.com 8.8.8.8

必要最低限の情報だけほしい場合

実行コマンド

+shortをつけるだけ!

1
$ dig ns google.com +short

実行結果

大分、目に優しい感じで表示されます

1
2
3
4
ns3.google.com.
ns2.google.com.
ns4.google.com.
ns1.google.com.

Please share  

メールが送信されるまでをパケットを見ながら解剖!

以前、コマンドだけでメールを送信する記事を書きましたが、
メール送信の一連の流れをパケットで追いかけていきたいと思います!

人物相関図


下記のソースを使えば、上記の図の構成でDocker環境を構築することができ、実際に動作を検証することができます。
https://github.com/kuniiskywalker/mail-tester

登場人物


送信君(送り手のメールサーバー)

IP:10.5.0.6
ドメイン:fuga.local


受信君(受け手のメールサーバー)

役割:メールサーバー
IP:10.5.0.5
ドメイン:hoge.local


案内マン(ネームサーバー)

IP:10.5.0.4


あらすじ(送信完了までの送信サーバーのパケットの流れ)

この図を元に解説していきます。

① 送信先メールアドレスのMXレコード確認


※MXレコード:メールの送信先IPアドレスが設定されたDNSレコード。

 送信君:

root@hoge.localに送信したいので、hoge.localのMXレコード教えてください

 案内マン: MXレコードは`mail.hoge.local`です。
 送信君:

ではmail.hoge.localのIPアドレスを教えてください。

 案内マン:

10.5.0.5です。

② TCPハンドシェイク


 送信君:

はじめまして受信君!
案内マンからIPアドレスきいてご連絡しました!
今からメールを送るためのやり取りしたいのですがいいですか?

 受信君:

はい大丈夫です!

 送信君:

ありがとうございます!

③ EHLO


※EHLOとはSMTP拡張機能を使うために送信者と受信者でお互いにサポートしてる機能をあわせて使用しましょうというやり取り。

 受信君:

メール受信を行う上での開始準備できてます!

 送信君:

それではまず、受信君はどういった機能をサポートしてるか知りたいので、教えてもらっていいですか?

 受信君:

↓こういうやつに対応してます!

SIZE:受信可能サイズ 10240KB

PIPELINE:SMTPで送信を行うための準備に使うコマンドを一括実行できる

他は割愛。。。

 送信君:

ありがとうございます!

PIPELINE対応してるんですねー、ではまとめて送信メールアドレスとか、送り元メールアドレスを送りますので、送信可能かどうか確認お願いします!

※MAIL FROM, RCTP TO, DATAコマンドを使って上記の確認を行う。

※1個ずつ実行していく場合は通信の利用効率が悪くなるため、こういった方式が導入されている。

⑤ 送信可否結果


 受信君: 結果をまとめてお返しします!
 受信君: ありがとうございます! 全部OKそうですね!

⑥ メール送信

 送信君: 結果うけとりました! メールを送信を許可いただきありがとうございます!

それではroot@hoge.local宛てに、差出人root@fuga.localとして、件名:test本文:This is test mail.でお送りします!

まとめ

登場人物も含めてやり取りをまとめてみました!

ちょっと駆け足で書いたので、ちょっとずつ修正します。。。

ローカル環境でメールの送受信の確認ができる環境をDockerでつくったのでご活用ください!
https://github.com/kuniiskywalker/mail-tester

Please share  

SNIのメモ

SNIのパケット覗き見

SNI(Server Name Indication)とは?

apacheのTLS通信用バーチャルホストをネームベース(ホスト名)で設定できる技術。
通常TLS通信の場合は、ホスト名も含めて暗号化されるからapacheは対応するバーチャルホストを見つけることができず、IPで見分けるしかなかった。

つまりIPひとつに対して443ポートが一つしか関連付けできないため、
事実上、IPに対してひとつしかTLSのバーチャルホストを設定できなかった。

そこで、ホスト名を暗号せずにブラウザから送信して、ネームベースでバーチャルホストの設定をできるようにする技術をSNI(Server Name Indication)と呼ぶらしい。

TLS拡張(RFC4366)
http://www.ipa.go.jp/security/rfc/RFC4366JA.html

検証したこと

  1. 俺俺TLS証明書
  2. httpd-ssl.confに以下バーチャルホストを設定
  3. chromeで https://test1.localhost にアクセス
  4. 実際パケット上でサーバー名がどうやって送信されるかwiresharkで確認

httpd-ssl.confの設定例

1
2
3
4
5
6
7
<VirtualHost *:443>
ServerName test1.localhost
DocumentRoot "/usr/local/apache2/htdocs"
SSLEngine on
SSLCertificateFile "/usr/local/apache2/conf/server.crt"
SSLCertificateKeyFile "/usr/local/apache2/conf/server.key"
</VirtualHost>

パケット

TLSハンドシェイクのClient Hello時(Client Helloについてはこちら)にTLS拡張で暗号化せずにserver_nameを送信してる。
おそらくapacheはこの情報をみてバーチャルホストを判定してる。

まとめ

パケットを見て分かるように、クライアントからservernameを暗号せずに送信してるため、当然のごとくクライアント側の実装依存になってしまい、旧ブラウザとか使えない(今はどうか知らない。。。)

apacheの設定をすることもめっきり減りましたが、ちょっとレンタルサーバーいじってて気になったのでメモしてみました(´・ω・`)

tlsハンドシェイクのメモ

tlsハンドシェイクとかわけわかめだったのでメモ。

やったこと

1. TLSの俺俺証明書作成

1
2
3
$ openssl genrsa 2048 > server.key
$ openssl req -new -key server.key > server.csr
$ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt

2. dokcerでapacheたてて、tcpdumpでパケットキャプチャー

apache用dockerコンテナー

https://github.com/kuniiskywalker/docker-test-capture-https

apache設定内容

DH鍵交換(DHE)無効

クライアントとサーバーで一時的な秘密鍵を生成して共通鍵を交換する安全な方法ですが、wiresharkで復号できないためRSAを使った鍵交換のみに設定。
但しクライアント側でもDHEを無効にしないと意味ない。

1
2
# SSLCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES
SSLCipherSuite kRSA
SSLのキャッシュ無効

キャッシュを利用されると二回目以降とかの通信でhttpsの通信の一部を省略されてしまうため無効

1
2
3
4
#SSLSessionCache         "dbm:/usr/local/apache2/logs/ssl_scache"
#SSLSessionCache "shmcb:/usr/local/apache2/logs/ssl_scache(512000)"
SSLSessionCache none
SSLSessionCacheTimeout 300

3. クライアントからcurlでアクセス

ブラウザの設定でDHEを無効にするのが面倒だったので、curlで検証しました。。。

1
2
# dokcerが動いてるvmによって以下アクセス先は要変更
curl --cacert server.crt https://localhost

4. キャプチャしたパケットをwiresharkで眺める

Wiresharkを開き、キャプチャしたファイルを開く。

通信が開始されるまでの大まかな流れ

Hello交換

事前にこういう方式で暗号化のやりとりしますよという取り決め

鍵交換

暗号化に使う鍵の交換
(実際はパラメーターを送信しあってお互いに共通の鍵を作る)

データ通信

利用開始!
実際は暗号化されていて中身は暗号化されていて、よくわからない文字列で埋め尽くされている

1.Hello交換

ざっくり言うと私(クライアント)が暗号化方式とか諸々提示しますので、あなた(サーバー)がどうやって暗号化するか決めてください。

というやりとりっぽい。

◯client hello

プロトコルバージョン

TLS1.0, 1.1, 1.2などの接続に用いるプロトコルを提示

(クライアント:TLS1.2で接続でお願いします!)

疑似乱数

クライアント側で生成した乱数をサーバーに送信

(クライアント:後で鍵交換に使う文字列なので控えといてください!)

暗号化方式

候補値(複数可)を提示

(クライアント:暗号化方式をこの中からお選びください!)

圧縮方式

候補値(複数可)を提示

(クライアント:圧縮方式は無圧縮でお願いします!)

※過去に色々問題があったため無圧縮一択らしい

拡張

候補値(複数可)を提示

(クライアント:その他もろもろの拡張に使用するものはこの中からお選びください)

※拡張に関しては割愛します

◯server hello

プロトコルバージョン

クライアントから提示された接続に用いるプロトコルに合意

(サーバー:TLS1.2で接続了解しました!)

疑似乱数

サーバー側で生成した乱数をクライアントに送信

(サーバー:後で鍵交換に使う文字列なので控えといて!)

暗号方式(暗号スイート CipherSuite)

提示された暗号化方式を選択

(サーバー:TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d)でOKです)

  • TLS_RSA:サーバーのRSA鍵を使った鍵交換アルゴリズム
    今回は強制的にRSAを使う設定にしたのでこれが選択された模様

  • AES_256_GCM:共通鍵暗号のアルゴリズム
    TODO:後で調べる

  • SHA384:MAC(メッセージ認証)のアルゴリズム
    TODO:後で調べる

圧縮方式

提示された圧縮方式を選択

(サーバー:無圧縮でOKです)

◯Certificate

サーバーの証明書情報をクライアントへ。

◯Server Hello Done

Server Hello 終了

2.鍵交換

暗号化する際に使用する鍵を生成する。

◯Client Key Exchange

クライアントで生成したキーをサーバーに送信する。

鍵交換アルゴリズム

1. RSA

クライアント側で生成した共通鍵生成に必要な情報を、サーバー側の公開鍵を使って暗号化して、
サーバーに送信する。
悪い人に暗号化したデータをずっと保存されて、ある日サーバーの秘密鍵が漏洩してすべて解読されてしまうというリスクがある。

ClientKeyExchangeのみ行われる。

上記Certificateでサーバーから取得した認証情報から公開鍵を取り出し、「ランダムな文字列」を暗号化してサーバーに送信する。

以下の部分が暗号化された文字列

このPreMaster Secretの値を使って後述するクライアントとサーバーで使用する共通鍵を作成するっぽい。

2. DHE

サーバーの公開鍵ではなく、お互いに一時的に生成した秘密鍵/公開鍵のペアで公開鍵を送り合って、
共通鍵生成に必要な情報を暗号化して送り合う。
ServerKeyExchangeとClientKeyExchangeの2回の通信で鍵交換を行います

一時的に作成される鍵なのでサーバーの秘密鍵が漏洩しても安心!

数学苦手だけど、以下みたいな感じです。

これでお互いに安全に秘密値(PreMaster Secret)を送り合えます。


DHEだと一時的に作成した鍵を使用するので復号が面倒くさい。
今回は簡単に復号できるRSAを使用します。

(上記server helloの暗号化方式でTLS_RSAに合意をしている)

共有秘密鍵の算出

これまでのやりとには、鍵を生成するための前準備でした。
これでクライアントとサーバー側で通信を暗号化するために使う鍵を生成するための情報が出揃いました。

ECDHEというアルゴリズムで算出してるみたいですが、割愛します!
ざっくりいうと以下の情報でいい感じに鍵を作るってこと見たいです。

・Hello Clientでクライアントが生成した乱数
・Server Helloでサーバーが生成した乱数
・PreMaster Secret

◯Change Cipher Spec

新しい暗号化仕様の利用開始を通知

これでTLSハンドシェイクは終了

2.データ通信

これで晴れて暗号化通信が出来るわけです。

暗号化されたデータ通信

リクエストデータ

レスポンスデータ

通信内容の復号化

上記のままだと暗号化されててわけわかめだから、以下の設定で事前に作成した証明書をインポートすることでTLS通信内容が復号化でき中身が確認できる。

Preference->Protocols->SSL->RSA Key List->Editを選択し、以下を設定。

IP Address-> 172.17.0.2
Port->443
Protocol->http
Key File->俺俺証明書key(server.key)

リクエストデータ

レスポンスデータ

雑感

ちょーざっくりメモでした

Please share  

場所が分かりにくいAmazonプライムビデオの視聴履歴リンクをメニューに表示させるchrome拡張機能

夏も早いものでもう終わろうとしている今日この頃、皆さんはどのような動画をみてお過ごしでしょうか?
僕はAmazonプライムビデオのラインナップの豊富さをいいことに動画三昧の日々を送っています。
そんなAmazonプライムビデオのありそうでなかったchrome拡張機能を作ってみました。

場所が分かりにくい視聴履歴へのリンク

Amazonプライムビデオを使っていて以下のように思ったことはありませんでしょうか?

  • ちょっと前に見てた動画のタイトルを思い出せない!
  • スマホで途中まで見てたやつをPCでみたい!
  • 過去に視聴した動画を見たい、

そんなとき視聴履歴を見たいという思いはは至極当たり前の心理だと思いますが、なぜか視聴履歴までたどり着くには以下のステップを踏まなければなりません。

  1. ヘッダーの「マイストアへ」をクリック

  2. 「おすすめ商品を正確にする」をクリック

  3. 画面左部の「視聴済みのビデオ」をクリック

そんな頻繁に見ることがないとはいえ、いざ見たいと思ったときにこのステップを思い出すのは至難の業です。。。

視聴履歴をみるための方法をgoogleで検索なんてしてたら時間が勿体無いですね。

たった一つの解決方法。。。

この視聴履歴の分かりずらさはサイトの回遊率を上げるためのものなのか、プライバシー的なものなのかはさておき、視聴履歴を見る方法だけで検索するとそれだけで記事になってしまいそうなニーズ感がある以上、この問題を解決するためのソリューションを提供することが僕の使命であることは自明である。

この問題をシンプルに解く方法、それは。。。。

そう、ヘッダーに「視聴履歴」のリンクを追加することです!

使い方

chromeをお使いの方限定なのですが、以下URLから拡張機能の追加を行ってください。

https://chrome.google.com/webstore/detail/amazon-prime-video-fast-h/cfifaldajbjpfiafieifemojpfdiccbm

インストール後、再度Amazonプライムビデオのサイトに訪れるとヘッダーのメニューに「視聴履歴」へのリンクが追加されているはずです!

それではよいAmazonプライムビデオ生活を!!

機能に関するお問い合わせは以下からお願いします。
https://github.com/kuniiskywalker/AmazonPrimeVideoFastHistory/issues

Please share  

MySQLのwait_timeout実験

秘伝のバッチが突如悲鳴を上げるということは少なくないと思いますが、自分の知識不足から復旧に時間を要してしまった。

自戒もかねてメモを残す。

やばいクエリによるtimeoutでgone away

“MySQL server has gone away” が発生する原因としては以下の要因が考えられる。

  • クエリのサイズがでかすぎ
  • 接続のアイドルタイムアウト

前者はmax_packet_sizeの値を調整して解決できる。
が、今回の原因は後者のタイムアウトの秒数だった。

接続が開始されてから、最後にデータベースに問い合わせをした時間から、指定した秒数たってしまうと、データベースはクライアントとの接続を勝手に切ってしまう。
接続が切れた状態でクエリを送信すると、「おまえの接続はもう切れてるぜ!」となりgone awayとなる。

この指定した秒数の謎についておいかけていきたいと思います(^O^)/

なぜ原因判明まで時間がかかってしまったのか

そもそも時間がかかりすぎるクエリがよくないのだが、明らかに設定された時間内より早いタイミングで切断されていた。

デフォルトだとおそらく2880秒で設定されている

1
2
3
4
5
6
7
8
9
% mysql -u root

mysql> SHOW VARIABLES LIKE 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout | 28800 |
+---------------+-------+
1 row in set (0.00 sec)

2880秒もたってないのになぜgone awayになるんだよ。。。

マニュアルに書いてあった。。。

以下公式からの引用

Interactive(対話型)で接続したやつはinteractive_timeoutの秒数をwait_timeoutとして設定しちゃうぜ!
ってことみたいです。

interactive_timeoutの秒数

1
2
3
4
5
6
7
8
9
% mysql -u root

mysql> SHOW VARIABLES LIKE 'interactive_timeout';
+---------------------+-------+
| Variable_name | Value |
+---------------------+-------+
| interactive_timeout | 28800 |
+---------------------+-------+
1 row in set (0.00 sec)

じゃあ対話型じゃない場合はどうなるかっていうと

1
2
3
4
5
6
7
% mysql -u root -e "show variables like 'wait_timeout'"

+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout | 60 |
+---------------+-------+

my.iniで設定されている数字がやっとでてきた。

アプリケーションから接続する際は非対話モード(Interactiveじゃない)になるので、
60秒が設定されていたため、早いタイミングで落ちていたみたいです。

28800秒で設定されるから他に原因があるのかと思い余計な時間をとられてしまった。。。

実験

前提

  • mysql 5.6
  • php7.1
  • pdo

実験1. wait_timeoutを5秒に設定して、DBに接続してからsleepを6秒いれた後、クエリを実行するとgone awayになるか確認。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
define('WAIT_TIME', 6);

try {
$pdo = new PDO('mysql:host=db;dbname=test;charset=utf8','root','pass',
array(PDO::ATTR_EMULATE_PREPARES => false));

sleep(WAIT_TIME);

$sql = "SELECT version();";
$pdo->query($sql);

} catch (PDOException $e) {
exit($e->getMessage());
}

見事gone away!

1
Warning: PDO::query(): MySQL server has gone away in /src/connect.php on line 11

実験2. interactive_timeoutを5秒に設定して、対話接続でクエリ実行後に5秒まち、再度クエリを実行するとgone awayになるか確認。

1
2
3
4
5
6
7
8
9
10
% mysql -u root

mysql> SHOW VARIABLES LIKE 'interactive_timeout';
+---------------------+-------+
| Variable_name | Value |
+---------------------+-------+
| interactive_timeout | 5 |
+---------------------+-------+
1 row in set (0.00 sec)

5秒待って再度クエリ実行

見事gone away!

1
2
mysql> SHOW VARIABLES LIKE 'interactive_timeout';
ERROR 2013 (HY000): Lost connection to MySQL server during query

テストに使ったコードはこちら

https://github.com/kuniiskywalker/test-mysql-goneaway

まとめ

対話型の場合だけ秒数が別指定が多めにとられてるのは、一旦接続していろいろいじることが前提なので当たり前といえば当たり前。。。

JSでバイナリを扱うことへの理解とメモ

ちょいちょい使うことがあるバイナリ操作処理だけど、イマイチ用語と用途が整理できていないのでメモ

背景

HTML5からバイナリが使えて便利最高!みたいな情報多いけど何が便利になってるのかよくわからない。
昔と比べてどう便利になったのかもふまえて整理してみる。

ゴール

JSでバイナリを使うことの利便性を享受できるようになる

まずバイナリが扱えるようになると何が嬉しいのか?

つーかバイナリって何?!

以下wikipedia引用

バイナリ(またはバイナリー:binary)とは、2進数のこと。 テキスト形式(文字データ)以外のデータ形式全般を指したり、コンピュータが処理・記憶するために2進化されたファイル(バイナリファイル)、またはその内部表現の形式(バイナリデータ、バイナリ形式)のことを指すのに用いられる。

つまり人間が読めないコンピューターだけが扱えるデータ形式

JSでバイナリが扱えなかった時代

  • バイナリを扱うためのAPIがなかったので、formタグでinput type=”file”を選択して送信するぐらいしかできなかった。
  • CSVやXML、画像ファイルなどのバイナリファイルはサーバー側に送って何かしらの処理を行っていた。

バイナリが扱えるようになった時代

  • HTML5以降にはいったFILE apiを使うことにより、バイナリの読み書きが行えるようになり、クライアント側だけで完結する処理が作れるようになった。
  • 画像をアップロードする前の画像をリサイズして送信したりとか、非同期で送信したりとか、アップロードするまえの画像をbase64エンコードしてimgタグに表示したりとかできて便利になった。

APIの種類と用途

Blob

  • バイナリデータを保持( immutable な生データ )
  • 値を取り出し不可( FileReaderやxhrなどを使用して中身をとりだせる)
  • サイズの確認や切り出しは可能(Blob.prototype.slice や Blob.prototype.size)
1
2
3
4
5
6
7
8
var req = new XMLHttpRequest();
req.open("GET", "https://i.imgur.com/rjI42fW.png", true);

req.onload = function(e) {
var blob = e.target.response; // これがBlob
};

req.send();

File

  • input type=”file”のファイル選択時にラップされるオブジェクト
  • Blobを継承している
  • Blobにファイル名や最終更新日の情報が追加されたもの( { name:String, lastModifiedDate:Date } )
    • ユーザーのシステム上のファイルをサポートするための機能を拡張
1
2
3
4
5
6
7
8
9
10
11
<input type="file" id="local_file">

input.onchange = function (event){
var input = event.target;
var file_list = input.files; // これはFileList
var file = file_list[0]; // これがFile

console.log(file.name);
console.log(file.lastModifiedDate);
};
</script>

ArrayBuffer

  • 物理メモリの領域(バッファ)を確保するためのクラス
  • バッファに対する操作はできない
    • バイト数取得と既存のバッファからあらたなバッファを作ったりしかできない
  • エンディアン方式がCPU依存
    • リトルエンディアンかビックエンディアンかを意識する必要がある
1
2
3
var buffer = new ArrayBuffer(12);

console.log(buffer.byteLength); // 12

Typed Array

  • 型付き配列
  • 型を指定してバッファから配列を生成
  • 「高速に」読み書きできる
  • 種類
    • Uint8Array
    • Uint16Array
    • Uint32Array
1
2
3
4
5
new Uint8Array([ (2 ** 8) -1 , 2, 3])
> Uint8Array(3) [255, 2, 3]

new Uint16Array([( 2 **16) - 1, 2, 3])
> Uint16Array (3) [65535, 2, 3]

DataView

  • TypedArrayよりも高機能なバッファ操作用クラス
  • ArrayBufferのエンディアン方式の差を吸収してくれる
    • リトルエンディアンかビックエンディアンかを意識する必要がない
1
2
3
4
var buffer = new ArrayBuffer(12);
var dataview = new DataView( buffer );

console.log(dataview);

BinaryString と DataURI と BlobURL

1
2
こういう形式のやつ
/9j/4AA~~~~~

DataURI

  • 画像のリソースをURIで表現できる
  • BinaryStringなのでサイズが1.3倍になる。。。
  • テキスト情報にバイナリデータをうめこめる(imgタグにうめこめる)
  • data: スキームが先頭についている URL
1
2
こういう形式のやつ
data:image/jpeg;base64,/9j/4AA~~~~~

FileReader使うと取得できる

1
2
3
4
5
6
7
8
9
10
11
12
<input type="file" id="local_file">

<script>
document.getElementById("local_file").onchange = function(event) {
var input = event.target;
var reader = new FileReader();
reader.onload = function(){
var dataURL = reader.result; // これがDataURI
   };
  reader.readAsDataURL(input.files[0]);
};
</script>

↓imgタグに埋め込めば画像も表示できる

BlobURL

  • 画像のリソースをURIで表現できる
  • セッション内でユニークなURIを生成する
  • DataURIとちがってリソースへのポインター的な扱いになるからメモリにやさしい
1
2
こういう形式のやつ
blob:http://kuniiskywalker.github.io/f6ea8265-610f-4fbe-abfa-335f13cfe34c

createObjectURLでURIを取得

1
2
3
var blob = new Blob( ["文字列テスト"] , {type: "text/plain"} );
var blob_url = URL.createObjectURL(blob); // これがBlobURL
console.log(blob_url); // blob:http://kuniiskywalker.github.io/f6ea8265-610f-4fbe-abfa-335f13cfe34c

まとめ

クライアントサイドでのオフライン対応が充実していく背景には、じつはこういった便利APIを知らず知らずのうちに使っていたことを改めて認識できた。
サーバーで仕事をさせるかクライアントで仕事をさせるかは、通信時のデータ転送量をとるか端末のCPUコストをとるか、要件次第な感じ。