Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 19005] ファイルの概要

このコミットは、Go言語の net パッケージにおけるUnixドメインソケットの挙動に関する修正です。具体的には、コネクションレスモードの UnixConn (例えば unixgram タイプ) が既に接続済み(Dial などで特定のピアに紐付けられている状態)であるにもかかわらず、宛先アドレスを明示的に指定してデータを書き込もうとした際に、正しくエラーを返すように変更されています。

コミット

commit 67a5181045d1601820ea98dff06832339e19fd16
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Wed Apr 2 19:42:05 2014 +0900

    net: make WriteTo, WriteToUnix and WriteMsgUnix fail when connectionless-mode UnixConn is already connected
    
    This CL tries to fill the gap between Linux and other Unix-like systems
    in the same way UDPConn already did.
    
    Fixes #7677.
    
    LGTM=iant
    R=golang-codereviews, iant
    CC=golang-codereviews
    https://golang.org/cl/83330045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/67a5181045d1601820ea98dff06832339e19fd16

元コミット内容

このコミットは、net パッケージ内の UnixConn 型に対して、WriteToWriteToUnix、および WriteMsgUnix メソッドが、コネクションレスモードの UnixConn が既に接続済みである場合に失敗するように変更を加えるものです。これにより、Linuxとその他のUnix系システム間での挙動のギャップを埋め、UDPConn が既に持っていた一貫した挙動に合わせることを目的としています。

変更の背景

Go言語の net パッケージは、様々なネットワークプロトコルとソケットタイプを抽象化して提供します。Unixドメインソケット(UDS)もその一つで、同じシステム上のプロセス間通信(IPC)に利用されます。UDSにはストリーム型(SOCK_STREAM)とデータグラム型(SOCK_DGRAM)があります。Goでは、unix ネットワークタイプがストリーム型に、unixgram ネットワークタイプがデータグラム型に対応します。

データグラムソケット(SOCK_DGRAM)は本来コネクションレスであり、各データグラムは独立して送受信され、宛先アドレスを明示的に指定する必要があります。しかし、connect() システムコールをデータグラムソケットに対して呼び出すことで、「接続済み」の状態にすることができます。この「接続」はTCPのような永続的な接続を確立するものではなく、以下の2つの効果をもたらします。

  1. デフォルトのピアアドレスの設定: 以降の send()write() システムコールで宛先アドレスを省略できるようになります。データは connect() で指定されたデフォルトのピアに送信されます。
  2. 受信パケットのフィルタリング: ソケットは connect() で指定されたピアからのデータグラムのみを受信するようになります。

問題は、Goの net.UDPConn では、Dial によって「接続済み」となったUDPソケットに対して、宛先アドレスを明示的に指定する WriteTo メソッドを呼び出すと、ErrWriteToConnected というエラーを返すように実装されていました。これは、ソケットが既に特定のピアに紐付けられているにもかかわらず、別の宛先への送信を試みるという矛盾した操作を防ぐためです。

しかし、net.UnixConnunixgram タイプでは、同様に Dial で「接続済み」となった場合でも、WriteToWriteToUnixWriteMsgUnix といった宛先アドレスを明示するメソッドが、一部のUnix系システム(Linux以外)で期待通りにエラーを返さない、または未定義の挙動を示す可能性がありました。この挙動の不一致が、異なるOS間でのアプリケーションの移植性や信頼性に影響を与える可能性があったため、このコミットで修正されました。

この修正は、Fixes #7677 と関連付けられていますが、直接的なIssueの内容は確認できませんでした。しかし、コミットメッセージと変更内容から、この不一致が報告され、その解決のために行われたものと推測されます。

前提知識の解説

Unixドメインソケット (Unix Domain Sockets, UDS)

UDSは、同じホスト上のプロセス間で通信を行うためのメカニズムです。ネットワークソケットと同様のAPI(socket, bind, listen, accept, connect, send, recv など)を提供しますが、ネットワークスタックを介さず、カーネル内部で直接データをやり取りするため、TCP/IPソケットよりも高速で効率的です。ファイルシステム上のパス名(例: /tmp/my_socket)をアドレスとして使用します。

UDSには主に以下の2つのタイプがあります。

  • ストリームソケット (SOCK_STREAM): 信頼性があり、順序が保証された、コネクション指向のバイトストリームを提供します。TCPに似ています。Goでは net.Dial("unix", ...)net.Listen("unix", ...) で使用されます。
  • データグラムソケット (SOCK_DGRAM): コネクションレスで、信頼性や順序は保証されませんが、メッセージの境界が保持されます。UDPに似ています。Goでは net.Dial("unixgram", ...)net.ListenPacket("unixgram", ...) で使用されます。

connect() システムコールとデータグラムソケット

通常、connect() システムコールはTCPのようなコネクション指向ソケットで接続を確立するために使われます。しかし、UDPやUnixデータグラムソケットのようなコネクションレスソケットに対しても connect() を呼び出すことができます。この場合の connect() は、実際の接続を確立するわけではありませんが、ソケットに「デフォルトのピアアドレス」を設定します。

この設定により、以下の挙動が変化します。

  • send()/write() の簡略化: sendto() のように毎回宛先アドレスを指定する必要がなくなり、send()write() を呼び出すだけで、デフォルトのピアにデータが送信されます。
  • 受信の制限: ソケットは、connect() で指定されたピアからのデータグラムのみを受信するようになります。他の送信元からのデータグラムは破棄されます。

WriteWriteTo の違い

Goの net パッケージでは、ソケットへのデータ書き込みに主に2つのパターンがあります。

  • Write(b []byte) (n int, err error): net.Conn インターフェースの一部であり、コネクション指向のソケット(TCP、Unixストリームソケット)や、connect() でデフォルトのピアが設定されたコネクションレスソケットで使用されます。データはソケットが接続している(またはデフォルトのピアが設定されている)相手に送信されます。
  • WriteTo(b []byte, addr net.Addr) (n int, err error): net.PacketConn インターフェースの一部であり、コネクションレスソケット(UDP、Unixデータグラムソケット)で主に使用されます。このメソッドは、送信するデータ b と共に、そのデータグラムの宛先アドレス addr を明示的に指定する必要があります。これにより、一つのソケットから複数の異なる宛先にデータグラムを送信できます。

ErrWriteToConnected

Goの net パッケージで定義されているエラーで、WriteTo メソッドが、既に connect() されている(特定のピアに紐付けられている)ソケットに対して呼び出された場合に返されます。これは、ソケットが既に特定の宛先を持つにもかかわらず、WriteTo で別の宛先を指定しようとする矛盾した操作を防ぐためのものです。

技術的詳細

このコミットの技術的な核心は、Unixデータグラムソケット(SOCK_DGRAM)が connect() された場合の WriteTo 系メソッドの挙動を、UDPConn と同様に一貫させることにあります。

従来の挙動では、Goの net.Dial("unixgram", ...) を使用して UnixConn を作成すると、内部的には SOCK_DGRAM ソケットに対して connect() システムコールが発行され、特定のピアに「接続済み」の状態になります。この状態の UnixConn に対して、WriteToWriteToUnixWriteMsgUnix といったメソッドが呼び出された場合、一部のUnix系システムでは、明示的に指定された宛先アドレスにデータが送信されてしまうか、あるいはエラーを返さないという挙動が見られました。これは、ソケットが既に「接続済み」であるという状態と、WriteTo が任意の宛先への送信を意図しているという機能との間に矛盾を生じさせます。

この不一致は、特にLinuxとその他のUnix系システム(例えばBSD系OSなど)の間で顕著であったと考えられます。OSのソケットAPIの実装詳細の違いにより、connect() された SOCK_DGRAM ソケットに対する sendto() の挙動が異なっていた可能性があります。

コミットは、このギャップを埋めるために、UnixConnWriteToUnix および WriteMsgUnix メソッドの内部にチェックを追加しています。具体的には、ソケットが SOCK_DGRAM タイプであり、かつ isConnected フラグが true の場合(つまり、Dial などで特定のピアに接続済みの場合)には、明示的な宛先アドレスが指定されていても、直ちに ErrWriteToConnected エラーを返すように変更されています。

これにより、Goアプリケーションは、Unixデータグラムソケットを使用する際に、OS間の挙動の違いを意識することなく、一貫したエラーハンドリングを行うことができるようになります。これは、堅牢で移植性の高いネットワークアプリケーションを開発する上で非常に重要です。

コアとなるコードの変更箇所

変更は主に src/pkg/net/unixsock_posix.go ファイルに集中しています。

--- a/src/pkg/net/unixsock_posix.go
+++ b/src/pkg/net/unixsock_posix.go
@@ -171,6 +171,9 @@ func (c *UnixConn) WriteToUnix(b []byte, addr *UnixAddr) (n int, err error) {
 	if !c.ok() {
 		return 0, syscall.EINVAL
 	}
+	if c.fd.isConnected {
+		return 0, &OpError{Op: "write", Net: c.fd.net, Addr: addr, Err: ErrWriteToConnected}
+	}
 	if addr == nil {
 		return 0, &OpError{Op: "write", Net: c.fd.net, Addr: nil, Err: errMissingAddress}
 	}
@@ -200,6 +203,9 @@ func (c *UnixConn) WriteMsgUnix(b, oob []byte, addr *UnixAddr) (n, oobn int, err
 	if !c.ok() {
 		return 0, 0, syscall.EINVAL
 	}
+	if c.fd.sotype == syscall.SOCK_DGRAM && c.fd.isConnected {
+		return 0, 0, &OpError{Op: "write", Net: c.fd.net, Addr: addr, Err: ErrWriteToConnected}
+	}
 	if addr != nil {
 		if addr.Net != sotypeToNet(c.fd.sotype) {
 			return 0, 0, syscall.EAFNOSUPPORT

また、src/pkg/net/unix_test.go には、この新しい挙動を検証するためのテストケースが追加されています。

コアとなるコードの解説

UnixConn.WriteToUnix メソッドの変更

WriteToUnix メソッドは、Unixドメインソケットにデータを書き込む際に、宛先アドレスを *UnixAddr 型で明示的に指定するものです。

追加された行:

	if c.fd.isConnected {
		return 0, &OpError{Op: "write", Net: c.fd.net, Addr: addr, Err: ErrWriteToConnected}
	}

このコードは、UnixConn のファイルディスクリプタ (c.fd) が isConnected フラグを持っているかどうかをチェックしています。isConnectedtrue の場合、それはこの UnixConnDial などによって特定のピアに接続済みであることを意味します。この状況で WriteToUnix が呼び出された場合、ソケットは既にデフォルトの宛先を持っているため、明示的に別の宛先を指定して書き込もうとするのは矛盾した操作です。したがって、OpError を作成し、その Err フィールドに ErrWriteToConnected を設定して返します。これにより、この操作が不正であることを明確に示します。

UnixConn.WriteMsgUnix メソッドの変更

WriteMsgUnix メソッドは、データだけでなく、補助データ(out-of-band data)も送信できる、より汎用的なUnixドメインソケットの書き込みメソッドです。これも宛先アドレスを *UnixAddr 型で明示的に指定できます。

追加された行:

	if c.fd.sotype == syscall.SOCK_DGRAM && c.fd.isConnected {
		return 0, 0, &OpError{Op: "write", Net: c.fd.net, Addr: addr, Err: ErrWriteToConnected}
	}

WriteMsgUnix の場合、WriteToUnix と同様の isConnected チェックに加えて、c.fd.sotype == syscall.SOCK_DGRAM という条件が追加されています。これは、このチェックがデータグラムソケット(SOCK_DGRAM)にのみ適用されるべきであることを保証するためです。ストリームソケット(SOCK_STREAM)は常に接続指向であり、WriteTo のような概念は適用されないため、このチェックは不要です。データグラムソケットが接続済みである場合に、明示的な宛先指定での書き込みを試みると、同様に ErrWriteToConnected エラーが返されます。

これらの変更により、Goの net パッケージは、Unixデータグラムソケットの「接続済み」状態における WriteTo 系メソッドの挙動を、UDPConn と同様に、より予測可能で一貫性のあるものにしています。

関連リンク

参考にした情報源リンク