見出し画像

片言の代理人 プログラム作成挑戦記その2

 初級者がプログラムの作成に挑戦する記事の第二弾の第二回です。前回の「まとめ」で述べたように、まずはプロキシから作成しました。

本題に入る前に

  一月いっぱいで倉庫の仕事をになってしまいました。
 年末に風邪を引き一週間ほど寝込みました。時間給なので働かなければ生活が立ち行かなくなります。そこで、年始からは不休で働いていました。
 しかし、無理がたたったのか、流感に罹ってしまい再び一週間ほど寝込みました。
 その結果、出勤日数が規程を下回り、アルバイト契約が更新されませんでした。
 と言う訳で、無職になってしまいました。プログラミングしほうだいです。やったね!

使用言語

 今回もC言語です。理由は、ネットワーク、マルチスレッドおよびI/O多重化の全てに対応しているのがC言語だけだったからです。...本当は、GoやRustに挑戦してみたかったのですが、参考書籍を買う余裕がありませんでした。

どのようなプロキシを作成したか?

通信の中継

 プロキシの最も基本的な機能の一つです。前回の記事に書いたように、私のネットワーク環境では、プロキシは、二つの防火壁の間に配置されます。プロキシは、壁の内側のクライアントからの要求を外部のサーバーに転送し、そのサーバーからの応答をクライアントに転送します。
 これにより、隔離されているクライアントが外部と通信できるようになります。

経路の振分け

 複数の受付ポートを設けて、各ポートごとにゲートウェイを切替える機能です。
 この機能は、プロキシとマシンの経路制御とにより実現されます。

マシンの経路制御
 NICに対し、複数(ゲートウェイと同数)のアドレス(10.0.1.1/24, 10.0.1.2/24)を割振ります。
 各アドレスごとに経路表を作成し、その経路表に所望のゲートウェイ(10.0.2.1, 10.0.2.2)を各々設定します。

プロキシ
 プロキシからサーバーへ送信を行う際に、所望のゲートウェイに対応したアドレスを送信元アドレスに設定します。
 例えば、ポート5001で受付けた要求にゲートウェイ10.0.2.1を使いたい場合は、そのゲートウェイ10.0.2.1に対応するアドレス10.0.1.1を送信元アドレスとしてデータを送信します。

 これにより、受付ポートごとにゲートウェイが切り替えられます。

並行処理

 今時のCPUは、たくさんのコアを備えています(私のパソコンは2コアですが...)。これらを有効に活用するために、マルチスレッドによる処理を行うようにしました。ただし、マルチスレッドの効果が薄い場合(同期の負担が重い場合)は、I/Oの多重化により処理します。
 本プログラムでは、以下の3種類のスレッドを動かしています。

受付スレッド(単数)
 クライアントからの要求を受け付けるスレッドです。要求があったときは、「クライアントからサーバーへと至る回線を敷設する」という仕事(命令)を待ち行列に格納します。受付スレッドは、格納するだけで実行はしません。実際に、実行するのは後述する作業スレッドです。
 受付スレッドは、シングルスレッドです。複数ポートの受付けは、I/Oの多重化処理により対応します。

監視スレッド(単数)
 回線を監視するスレッドです。回線にデータが到着したときは、「データを転送する」という仕事を待ち行列に格納します。監視スレッドも受付スレッドと同様に格納するだけです。
 監視スレッドは、シングルスレッドです。複数回線の監視は、I/Oの多重化処理により対応します。

作業スレッド(複数)
 待ち行列から仕事(命令)を取り出し、それに従い作業を行うスレッドです。
 仕事は、上述したように回線の敷設とデータ転送です。データ転送は、httpとhttpsで処理単位が異なります(プロトコルについては大文字のHTTPで、スキームについては小文字のhttpで表します)。

 httpsの場合は、要求若しくは応答又はそれらの部分が最小処理単位になります。つまり、一つのスレッドは、小分けにされたデータの一部を一方向にしか転送しません。そのため、複数のスレッドによる並列処理(分割データの同時転送や送受信同時転送)が可能になります。

 httpの場合は、最小処理単位が要求および応答になります。つまり、一つのスレッドが、ある要求の転送から、それに対する応答の転送までを受け持ちます。そのため、一つの要求/応答については、並列処理を行えません。なお、複数の要求/応答については、複数のスレッドによる並列処理が可能です。

 作業スレッドは、マルチスレッドです。実装では、スレッドプールを作り、そのスレッドを状態変数で叩き起こしています。

L4ブリッジとL7ブリッジ

 HTTPはTCP上のプロトコルです。そこで、中継方法として、L4のTCPで中継する方法と、L7のHTTPで中継する方法とが考えられます。

 L4で中継する場合、基本的にはHTTPの解釈を行いません(接続先の取得を除く)。L4の中継器(ブリッジ)は、受信したデータをそのまま送信先に丸投げします。HTTPではmethodがCONNECTのとき、プロキシはL4ブリッジとして振る舞います。

L4ブリッジ

 L7で中継する場合、HTTPを解釈して、つまり意味を理解して、通信を行います。そのため、例えば、独自にデータ圧縮を行うことが可能です。

作成難易度

 難易度は、解釈を行わないL4ブリッジのほうが圧倒的に低いです。そこで、L4ブリッジを作成したいと思います。
 ...と、作成に取り掛かる前は考えていました。
 前回の記事を書いた時点では、HTTPをよく理解していなかったので、プロキシを利用するときのmethodは全てCONNECTになると勘違いしていました。
 しかし、実際には、CONNECTはhttpsでしか使われず、httpではGETやPOSTが使われます。
 したがって、HTTPプロキシはL7ブリッジでなければなりません。
 つまり、httpsだけでなくhttpにも対応できる「まとも」なプロキシを作成するためには、HTTPの解釈を実装しなければならないということです。
 なお、HTTP/2以降はover TLSが前提になっているので、実装対象はHTTP/1.1になります。

HTTP/1.1

 HTTP/1.1は、とにかく面倒くさかったです。あれは、予想外に普及してしまった稚拙な規格を無理やり拡張して出来上がった妥協の産物(産業廃棄物)です。はやく消えてほしいです。

手を抜いたところ

methodの実装

 GET、POST、CONNECT以外のmethodは実装していません。Web閲覧等の普通の使い方では、他のmethodは使わないからです、多分。 

Hop-by-Hopヘッダの消尽

 Hop-by-Hopヘッダは読まずに削除しています。特に、OCSPの応答でKeep-Aliveの時間が指定されていることがあるのですが、無視して切断しています。

まとめ

 予想していたよりも大規模なプログラムになってしまいましたが、無事に完成しました。規模の大小よりも、大変だったのはHTTP/1.1です。基本設計がダメなものは、何をやってもダメですね。

副産物
 今回のプログラムでは、10〜20のスレッドが同時に動作することがあります。そのような状態でログを取るには、スレッドの同期が必要になります。そこで、マルチスレッド対応のロガーを作成しました。
 また、プロキシの初期設定を行うために、以前作成した設定ファイルの解析器を使用しています。
 徐々にオレオレライブラリが充実しつつあります。
 しかし、これはよいことではありません。
 なぜなら、開発言語がC言語だからです。C言語は、主たる開発言語にふさわしくありません。C言語には欲しい機能が欠けています(継続、例外、関数の部分適用やオーバーロード、関数内関数、ラムダ記法、本物の総称型、パッケージなどなど)。
 はやく次の開発言語に移行しなければ、C言語沼に引きずり込まれそうです。

こんなはずではなかった
 題名の「片言の代理人」とは、当初作成するつもりだったL4ブリッジのことです。L4ブリッジなら初級者の私でも簡単に作成できるはずでした。
 しかし、上述したように実際にはL7ブリッジを作成するはめになりました。知っていたらsquidを置き換えるなどというバカなことは考えなかったでしょう。
 あと、今更ですが、L4ブリッジで十分ならば、HTTPではなくSOCKSのプロキシを作成すべきでした。

 次は、仮想環境の作成記事を予定しています。基本的には、iproute2をNetlinkで書き換えるだけなので、簡単です!

古往今来得ざれば即ち書き得れば即ち飽くは筆の常也。と云うわけで御座います、この浅ましき乞食めに何卒皆々様のご慈悲をお願い致します。