双方向通信をPiping Server上で実現するCLIとGoライブラリ(E2E暗号化付き)
#Piping_Duplex #Piping_Server
やりたいこと
Piping Server上で双方向通信を行いたい。
動機としては、リモートでの作業が増えてる中、安全な通信を確保したい。
暗号通話をする準備としてPiping Server上で双方向通信をするを再利用できるCLIやライブラリの形で残しておきたい。
Piping Serverで双方向といえばこの記事。
Piping Server を介した双方向パイプによる,任意のネットワークコネクションの確立 - Qiita
記事の最後はポート開放せずにHTTP上でSSHを実現している。
以下で開発している。CLIとして、ライブラリとして使えその使い方などをこのページ書く。
https://gh-card.dev/repos/nwtgck/go-piping-duplex.svg https://github.com/nwtgck/go-piping-duplex
インストール
以下で、Windows, macOS, Linux向けにワンバイナリを配布している。
シンプルな例
以下のようにして使う。IDは任意で衝突を避けるため長くすることができる。
$ piping-duplex 自分のID 通信相手のID
以下のデモのように、piping-duplexコマンドを使うと
自分の標準入力が相手の標準出力に、
相手の標準入力が自分の標準出力に流れる。
https://gyazo.com/d35f52b37079b8d5baef8b58732255b4
これだけでもリアルタイムに流れる簡易チャットのようになる。
無限
無限のバイトストリームが扱える。
例えば、AさんとBさんが、以下のコマンドを打つ例。
Aさん:
$ cat /dev/urandom | ./piping-duplex aaa bbb
Bさん:
$ seq inf | ./piping-duplex bbb aaa | base64
https://gyazo.com/d65a9efaf7a96a613c120fd785855de2
Aさんから流れる/dev/urandomはBさんに届きそのあとbase64コマンドで文字列にする。
Bさんから流れるseq infも無限に流れるがそれもAさんに届く。
SSHする
テキストやランダムバイト列に限らず、意味のあるプロトコルを流すこともできる。
例えば、「Piping Server を介した双方向パイプによる,任意のネットワークコネクションの確立 - Qiita」で紹介されているSSHする方法もpiping-duplexコマンドを使っても可能。
以下をSSHサーバーが立っているサーバーで叩く。
$ socat 'EXEC:./piping-duplex aaa bbb' TCP:127.0.0.1:22
以下をSSHクライアント側で叩く。
$ socat TCP-LISTEN:31376 'EXEC:.piping-duplex bbb aaa'
以下をSSHクライアント側の新しいターミナルで叩く。
$ ssh -p 31376 localhost
上記の一連のコマンドでポートを開放せずにHTTP上(Piping Server上)でSSHができる。
エンドツーエンド暗号化 (E2E暗号化)
E2E暗号化したいときは、-cオプションまたは--symmetricオプションをつけてパスワードで端末間で暗号化できる。
HTTPSで暗号化されているものの、サーバーすら信用せずに安心して利用するためにE2E暗号化が欲しくなる。
内部的にはOpenPGPを使っている。
詳細: 「Go言語でOpenPGPの共通鍵暗号化と復号をストリーミングしながら行う」
piping-duplexコマンドはコマンドなので、パイプを使って他のopensslコマンドなど好きな技術を組み合わせて暗号化するなども可能。
手軽に暗号化できるように、オプションも作った。
オプションの由来はgpgコマンド。
https://gyazo.com/ea0ab0da2afce70e1b854ac0b4ea6dbc
今後、Piping UIと同じ方式で、パスワードなしでのE2E暗号化も搭載させるかもしれない。
経由させるサーバーを指定
-sまたは--serverオプションで変更可能。
$ piping-duplex -s https://piping.glitch.me aaa bbb
Piping Server自体もいろんな方法でセルフホスト可能。
以下はDockerでの例。(にたつ)
$ docker run -p 8181:8080 nwtgck/piping-server
ライブラリとして使う例や応用例
github.com/nwtgck/go-piping-duplex をライブラリとして使う例。
code:go
// v0.2.0時点での使い方
import (
"github.com/nwtgck/go-piping-duplex"
"os"
"strings"
"io"
)
func main() {
server := "https://ppng.io"
aId := "aaa"
bId := "bbb"
go piping_duplex.Duplex(server, aId, bId, strings.NewReader("hello, world\n"))
r, _ := piping_duplex.Duplex(server, bId, aId, strings.NewReader("hoge"))
// A -> B に送られた "hello, world\n" を標準出力に表示
io.Copy(os.Stdout, r)
}
piping_duplex.Duplex(..., ..., ..., r)は相手に届けたいio.Reader rを渡して、相手からの応答 io.Readerが戻り値になる関数。
例えば、自分の音声をマイクから集音したio.Readerを渡して、相手の音声のio.Readerをスピーカーに出力すると原理的には通話ができる。エコーキャンセラがあるとより実用的になる印象だった。
音に限らず、カメラ映像を送って、相手のカメラ映像を画面に出力するとビデオ会議的なものが実現できるはず。
現状でも以下のようにrecコマンドとplayコマンドでpiping-duplexコマンドを挟むと通話ができる。実際にやってみるとエコーキャンセラの技術を学ぶ必要を感じる。
$ rec -t wav -c 1 -r 16k - | ./piping-duplex aaa bbb | play -t wav -
仕組み
Piping Server経由のHTTPリクエストを2つ送っているだけ。
つまり、
AさんはPOST /aaaとGET /bbbをして、
BさんはPOST /bbbとGET /aaaをしている。
きっとシェルスクリプトでも実装できる。(E2E暗号化付きにしてワンバイナリで配布したかった)
Piping Serverは、POST /hogeに送ったものが、GET /hogeで受け取れるというシンプルな仕様。
得体の知れないプロトコルではなく、よく知られたHTTPの上で動くという安心感はある。
HTTPはオーバースペックなのか?について
まず、2つリクエストを送っていてもHTTP/2に対応しているため、単一のTCPコネクションにまとまる。
また、HTTP/2だとネイティブでストリームに対応していて、Transfer-Encoding: chunked もより最適になるという風に理解している。
詳細: HTTP/2では「Transfer-Encoding: chunked」を使ってはいけない
HTTPがオーバースペックだと言われるのは、ヘッダが大きいという理由が多いと思う。Piping Duplexの使い方はとしては接続しっぱなしの2つのリクエストだけ、最初にヘッダ送られた後無限のボディが続く。
HTTPの接続がどれだけ続くのかに関しての疑問も出てきそう。それに関しては、以下が一つの答えかも知れない。
87日間連続でリモートのPiping Serverに無限にデータ転送して45.1TB転送できてる図
87日間同じHTTPリクエストが継続して、データ的には45TB送れているという実験。この手の実験は何回かやっている。
3ヶ月近く同じHTTPリクエストできいる。普通電話を同じ人と3ヶ月間することも少ない。HTTP/3でUDP上で動くようになったり、コネクションマイグレーションでIPアドレスの変更があっても接続が維持されたりするらしい。HTTPは枯れた安定した技術でありながら、進化もしていて、生HTTPにも可能性を感じている。