H8OS(ver.3.5.1) のTCPソースをトレースしました。
やはり再送パケットを受信した場合が想定されていないらしく、再送パケットを受けたりするとそのコネクションはフリーズしてしまいます。以下ではTCPのシーケンス番号およびACK番号について、H8OSの仕様と、その問題点および対処方法をまとめます。
またこの改修により、改修前とどのような動作の違いがでるのかをこちらでケーススタディしています。


【TCP処理の流れ】
OSのマニュアルを読めばわかりますが、TCPパケット受信時は図のような動作になります。UNIXで用いられているsocketとは利用方法が異なりますが、一方で、処理は簡便でソースもコンパクトです。
図1:H8OSのTCP処理シーケンス

【シーケンス番号】
TCPを受信すると、tcp関数のスタック上一時変数として構造体tcp_headerのtcp変数が確保されます。この変数には受信したTCPパケットのヘッダ情報が設定されています。
また、グローバル変数として確保された構造体tcpinfoの配列であるconnet変数が確保されており、その添字でコネクションを識別します。tcpinfoにもシーケンス番号を記憶するメンバが定義されており、tcp_socket関数が呼ばれた時に初期値が設定されます。以降はtcp関数を呼ばれた際に状態に応じた論理で値が更新されますが、これについては後述します。
送信時にはset_tcp_header関数が呼び出されたときに、connect変数に保持されている値を、送信TCPヘッダの領域に設定します。
図2:H8OSのTCPシーケンス番号のデータ相関

【TCP関数における処理】
下記の表はTCP関数で実行されているシーケンス番号の処理をまとめたものです。 縦軸はTCPフラグ、横軸はTCP状態で、それらの組み合わせ時にconnet変数のrmseqおよびmyseqにどのように値が設定されるかを示しています。
表中の記号の意味は次の通りです。
  • <#○-○>:図2に記載の処理に対応つけています。
  • [...]:APLのtcpproc(図1参照)を呼び出す場合の第一パラメータの値です。
  • 上記以外:呼び出される関数名です。
図3:H8OSのtcp関数におけるシーケンス番号設定

【問題点】
TCPの再送を考えると図2や図3の<#1-1>や<#1-2>の処理には問題がありそうです。なぜならこれらの処理は、グローバル変数のconnectに保持されていた値をインクリメントするため、最初のパケットと再送パケットを受けた時ではその値が異なってしまいますし、続くパケットのシーケンス番号もずれてしまいます。
これに対処するには、再送パケットを受けても廃棄するか、再送パケットの処理を行うがシーケンス番号は最初のパケットと同じものを返却するようにするかのいずれかになります。前者は一見容易そうですが、課題はすでに受信したetherフレームを空読みするなどで廃棄しなくてはなりませんし、応答がネットワークで紛失したことによって起きた再送は取り込む必要もあるので、廃棄するだけでなく再送処理も必要になります。そこで、今回は、再送処理はAPLに委ねることとして、後者の対処を入れることにしました。

【修正点】
図3においてrmseq変数をインクリメントしている処理がありますが、これでは再送時にも余分なインクリメントを行ってしまう問題があることがわかります。そこで
SYNを受信したときは受信したシーケンス番号に1を加える処理にします。
また、コネクション確立時のSYN+ACKが紛失すると、再度コネクション確立のSYNを受けますが、すでにH8OSはTCP_SYNRCVDに遷移しているため、その状態でのSYN受信は無視してしまいます(詳しくはこちら)。そこで
TCP_SYNRCVD状態でのSYN受信にはSYN+ACKを返送するように修正します。
コネクション確立時の最後のACKが紛失した際にも同様の問題があるので(詳しくはこちら)、
TCP_SYNRCVD状態でデータパケットを受信したらAPLヘデータ受信を通知するように修正します。
ただしこの修正により、コネクション確立時の最後のACKが紛失した際にはAPLにはBEGINが通知されなくなります。
図4:tcp関数におけるシーケンス番号設定の改造点


戻る