UDPおよび応答配信の問題

画像

以下は、ネットワークアプリケーションでudpを使用する問題に関する記事の翻訳です。 翻訳者は、ソーステキスト、他のネットワークアドレス、ルビーコードなどの例を変更することを許可しました。 翻訳では、真珠の簡単なスクリプトを使用しました。 問題の本質とこれによる解決策は変わりません。

さらに、コメントがカッコ内、斜体で追加されました。

注目を集めるための写真は、素晴らしい本「 learnyousomeerlang.com 」のテキストから取られています



軽量プロトコルのハードワーク





接続を確立しないプロトコルは、それらが引き起こす混乱全体を正当化しないと思われることがあります。



例として、最初のリクエストを含むUDPデータグラムがインターフェース上の別のIPアドレス(エイリアスまたはセカンダリIP)に送信されたときに応答が受信されたときの状況を分析してみましょう。

eth1インターフェースがあります:

$ ip a add 192.168.1.235/24 dev eth1 && ip a ls dev eth1 2: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 link/ether 00:30:84:9e:95:60 brd ff:ff:ff:ff:ff:ff inet 192.168.1.47/24 brd 192.168.1.255 scope global eth1 inet 192.168.1.235/24 scope global secondary eth1 inet6 fe80::230:84ff:fe9e:9560/64 scope link valid_lft forever preferred_lft forever
      
      







udpでパッケージを受け取るためのコードは通常どのようなものですか? まあ、エコーサーバーは猫の下のものに非常に似ているかもしれません:

echo_server.pl
 #!/usr/bin/perl use IO::Socket::INET; # flush after every write $| = 1; my ($socket,$received_data); my ($peeraddress,$peerport); $socket = new IO::Socket::INET ( MultiHomed => '1', LocalAddr => $ARGV[0], LocalPort => defined ($ARGV[1])?$ARGV[1]:'5000', Proto => 'udp' ) or die "ERROR in Socket Creation : $! \n"; print "Waiting for data..."; while(1) { $socket->recv($recieved_data,1024); $peer_address = $socket->peerhost(); $peer_port = $socket->peerport(); chomp($recieved_data); print "\n($peer_address , $peer_port) said : $recieved_data"; #send the data to the client at which the read/write operations done recently. $data = "echo: $recieved_data\n"; $socket->send("$data"); } $socket->close();
      
      









これはかなり単純な真珠のスクリプトで、udpパケットの送信者、パケットの内容を表示し、送信者にパケットを送り返します。 どこも簡単です。 サーバーを実行します。

 $ ./echo_server.pl Waiting for data...
      
      





彼が聞いているものを見てみましょう:

 $ netstat -unpl | grep perl udp 0 0 0.0.0.0:5000 0.0.0.0:* 9509/perl
      
      





その後、リモートマシンからプライマリIP経由でサーバーに接続します。

 -bash-3.2$ nc -u 192.168.1.47 5000 test1 echo: test1 test2 echo: test2
      
      





私たちのマシンのtcpdumpでどのように見えるか(まあ、または見えるはずです):

 -bash-3.2$ tcpdump -i eth1 -nn port 5000 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth1, link-type EN10MB (Ethernet), capture size 96 bytes 17:41:00.517186 IP 192.168.3.11.44199 > 192.168.1.47.5000: UDP, length 6 17:41:00.517351 IP 192.168.1.47.5000 > 192.168.3.11.44199: UDP, length 12 17:41:02.307634 IP 192.168.3.11.44199 > 192.168.1.47.5000: UDP, length 6 17:41:02.307773 IP 192.168.1.47.5000 > 192.168.3.11.44199: UDP, length 12
      
      





ただ素晴らしい-私はパッケージを送り、パッケージを取り戻します。 netcatでは、何を印刷しても返されます(「矢印」を印刷すると、とんでもない効果が得られます)。



そして今、同じインターフェイスのセカンダリアドレスに同じこと:

 -bash-3.2$ nc -u 192.168.1.235 5000 test1 test2
      
      





今回のtcpdumpでのこの狂気の様子:

 -bash-3.2$ tcpdump -i eth1 -nn port 5000 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth1, link-type EN10MB (Ethernet), capture size 96 bytes 17:48:32.467167 IP 192.168.3.11.34509 > 192.168.1.235.5000: UDP, length 6 17:48:32.467292 IP 192.168.1.47.5000 > 192.168.3.11.34509: UDP, length 12 17:48:33.667182 IP 192.168.3.11.34509 > 192.168.1.235.5000: UDP, length 6 17:48:33.667332 IP 192.168.1.47.5000 > 192.168.3.11.34509: UDP, length 12
      
      







もちろん、ポートが正しい場合でも、完全に馴染みのないアドレスからパケットを受信する自尊心のあるネットワークスタックはありません。 したがって、クライアントは戻りパケットを受信することはなく、サーバーが単にリクエストをドロップしていると考えます。



一見すると何が起こるかは、まったくのナンセンスに思えます。 しかし実際には、これは、UDPなどのセッションを設定しないプロトコルの一般的な欠陥です。 ご覧のように、ソケットは任意のアドレスをリッスンします(空のLocalAddrパラメーターは、使用可能なすべてのソケットを作成するときに「0.0.0.0」という形式のアドレスとしてシステムに渡されます。これにより、使用可能なすべてのアドレスでソケットがリッスンされます。これは特に直感的なアクションではありません)。 socket-> recv()を使用してアプリケーションでパケットを受信すると、パケットが送信された特定のアドレスに関する情報を受信しません。 私たちは、オペレーティングシステムがパッケージが私たちのためであると判断したことだけを知っています(ここでカプセル化があります)。 わかっているのは、パッケージの出所です。 そして、カーネルはソケットの接続に関する情報を保存しないという事実のため(カーネルは論理的であり、接続なしで要求されました-接続なしになります)、パケットを返送するときが来たとき、私たちに伝えることができるのはパケットの送信先です。 (真珠では、これは暗黙的に行われます。データグラムの送信者のアドレスとポートは$ソケットオブジェクトに関連付けられているため、送信呼び出しで指定する必要はありません)。

しかし、本当のパズルは、応答データグラムに送信者アドレスを書き込もうとするときに始まります。 ここでも、接続なしで作業するため、カーネルは送信者または受信者に関する情報を保存しません。 また、「任意の」インターフェイスをリッスンするため、オペレーティングシステムは、「好きな」アドレスからパケットを送信するための単純なブランチがあると考えています。 Linuxでは、パケットの送信元のインターフェイスのメインアドレスが選択されているようです。 (実際、アドレスはRFC1122、3.3.4.2「マルチホーミング要件」に従って決定されます。これは、ルーティングテーブル-翻訳者のメモによる) 「1つのアドレス-1つのインターフェイス」という一般的なケースでは、すべてが機能します。 しかし、あまり一般的でない状況になるとすぐに、ニュアンスが現れ始めます。

解決策は、特定のアドレスをリッスンするソケットを作成することです。 そして、これらのソケットからパケットを送信します。カーネルは、どのアドレスからパケットを送信したいかを認識し、すべてがうまくいきます。 とてもシンプルに見えますよね? もちろん、健全なネットワークアプリケーションは既にそれを行っていますよね? したがって、RubyでのUDP実装がただのたわごとであることは明らかです(元のソースコードはrubyにあります、翻訳者のメモです) 。 それは私が最初に考えたことであり、あなたが同じと思った場合、私はあなたを責めません。 しかし、RubyのUDPSocketの作者との戦争のRubiconを超えるまで、他の一般的に使用されるアプリケーションで少し実験してみましょう。 たとえば、SNMPd。 ubuntのnet-snmpdパッケージのデーモンには、上記のテストアプリケーションと同じ問題が発生します。 これはある種の新しいレーキであるとは思われません。彼らはただ足を踏み入れて、修正のためのパッチがたくさん散らばっています。

それで、一般に、誰もが同じ「病気」に苦しんでいます。 「全員」とは「一部のUDPサーバー」を意味します。 インターフェイス上のエイリアスに関する同様の問題の影響を受けないソフトウェアがある程度あります。 バインドはすぐに思い浮かび、すべてのインターフェイスを設定した後にNTPdを起動すると正常に機能します。 違いは何ですか? 違いは、これらのサービスは多少「スマート」であり、システム内のすべてのアドレスに個別にバインドすることです。 たとえば、バインド:

 $ netstat -lun |grep :53 udp 0 0 192.168.1.47:53 0.0.0.0:* udp 0 0 192.168.1.47:53 0.0.0.0:* udp 0 0 127.0.0.1:53 0.0.0.0:*
      
      





これは非常にクールで、問題を解決します。 例外は、デーモンの起動後に追加のエイリアスを追加する場合です。 バインドは新しいアドレスを取得しないため、サーバーを再起動する必要があります。 さらに、プログラム内で多数のソケットを使用する必要があるため、コードが少し複雑になります(たとえば、受信しようとするとブロックするのではなくselect()を使用します)。一般に、不必要な困難を好む人はいませんが、これを処理できます。 ただし、実際の問題は「デーモンの起動後にアドレスを追加しない」というルールです。 IPアドレスがシステムに追加されているかどうかを確認し、アドレスを追加した後にサービスを再起動する必要が実際の問題になります。

ただし、この問題の回避策もいくつかあります。 ここでは、ntpdを思い出します。 彼がリッスンするポートは次のとおりです。

 $ netstat -nlup | grep 123 udp 0 0 192.168.1.235:123 0.0.0.0:* udp 0 0 192.168.1.47:123 0.0.0.0:* udp 0 0 127.0.0.3:123 0.0.0.0:* udp 0 0 127.0.0.1:123 0.0.0.0:* udp 0 0 0.0.0.0:123 0.0.0.0:* udp6 0 0 fe80::230:84ff:fe9e:123 :::* udp6 0 0 ::1:123
      
      







NTPdは各アドレスを個別にリッスンし、さらにシステムで利用可能なアドレスをリッスンします。 これがなぜ必要なのか正確にはわかりません。 各アドレスを個別にリッスンするだけの場合、バインドの場合と同様に、すべてがうまくいきます。 ただし、ntpdの起動後に別のアドレスをインターフェイスに追加すると、udp-echoサーバーの場合と同様に、同じ問題が発生し始めます。 だから、「どんな」インターフェースで聞いてもプラスになるとは思いません。 ただし、これにより、ntpdの動作はBindとは多少異なります。Bindの開始後に追加されたインターフェイスにパケットを送信すると、単に無視されます(要求をリッスンするソケットはありません)。 Ntpdは応答を送信しようとしますが、応答のアドレスが正しくないという問題に悩まされています。 (ただし、インターフェイスのプライマリアドレスを変更し、新しいインターフェイスを作成できます。トランスレータに注意してください)。



現時点では、最良の解決策はBindとntpdのパスをたどり、ntpdからの「フォーカス」を使用してすべてのアドレスを個別にリッスンするようです。 同時に、0.0.0.0でパケットを受信した場合、システムで使用可能なアドレスのスキャンを開始し、それらに追加でバインドする必要があります。 これで問題が解決するはずです。

それを機能させるためだけに残っています(そして、おそらく途中で出てくる多くの問題を解決します)。 幸運を祈ります。 どこにいても聞こえる痛みと苦痛の叫びはおそらく私のものです。



UPD: Quasar_ruからの興味深い説明がコメントにありました 。 それでも、スクリプト言語でのUDPの実装はあいまいです。純粋なCでは、別のアドレスからサーバーからの応答を受信できるクライアントアプリケーションを作成できます。 このような実装の利点は議論の余地がありますが、実装は可能です。



All Articles