FreeBSD kernel SOCKET I/F 探検

第17回 bind() システムコール (5)

2015.04.22 (2015.05.28更新)

必要とする知識など:
  • UNIX (含む Linux) 上での C 言語と SOCKET によるプログラミング経験および TCP/IP に関する知識。

bind() システムコール、五回目です。sys_bind() ->kern_bind() -> sobind() -> udp_bind() -> in_pcbbind() -> in_pcbbind_setup() を呼び出して、in_pcbbind_setup() の途中からです。

        laddr = sin->sin_addr;
        if (lport) {
                struct inpcb *t;
                struct tcptw *tw;

                /* GROSS */
                if (ntohs(lport) <= V_ipport_reservedhigh &&
                    ntohs(lport) >= V_ipport_reservedlow &&
                    priv_check_cred(cred, PRIV_NETINET_RESERVEDPORT,
                    0))
                        return (EACCES);
                if (!IN_MULTICAST(ntohl(sin->sin_addr.s_addr)) &&
                    priv_check_cred(inp->inp_cred,
                    PRIV_NETINET_REUSEPORT, 0) != 0) {
                        t = in_pcblookup_local(pcbinfo, sin->sin_addr,
                            lport, INPLOOKUP_WILDCARD, cred);
/*
 * XXX
 * This entire block sorely needs a rewrite.
 */
                        if (t &&
                            ((t->inp_flags & INP_TIMEWAIT) == 0) &&
                            (so->so_type != SOCK_STREAM ||
                             ntohl(t->inp_faddr.s_addr) == INADDR_ANY) &&
                            (ntohl(sin->sin_addr.s_addr) != INADDR_ANY ||
                             ntohl(t->inp_laddr.s_addr) != INADDR_ANY ||
                             (t->inp_flags2 & INP_REUSEPORT) == 0) &&
                            (inp->inp_cred->cr_uid !=
                             t->inp_cred->cr_uid))
                                return (EADDRINUSE);
                }

順番に見ていきます。

laddr = sin->sin_addr;

関数内のローカル変数 struct in_addr laddr に、同じく関数内のローカル変数 struct sockaddr_in *sin の sin_addr を代入しています。sin には当関数の第二引数 struct sockaddr *nam が代入されています。結局、laddr には、server.c で bind() システムコールの第二引数で渡した struct sockaddr_in sa の sin_addr (All 0 == INADDR_ANY) が代入されます。

次の処理に進みます。

if (lport) {

lport は、server プログラム起動時の第一引数で指定したポート番号です。今後の説明の便宜上 8000番 を指定されているものとします。ということで、この if 文は真となります。

if (ntohs(lport) <= V_ipport_reservedhigh &&
    ntohs(lport) >= V_ipport_reservedlow &&
    priv_check_cred(cred, PRIV_NETINET_RESERVEDPORT,
    0))
        return (EACCES);

if 文の中で使用している ntohs() は、man BYTEORDER(3) にてその機能を確認できます。以下に一部抜粋しておきます。

BYTEORDER(3)           FreeBSD Library Functions Manual           BYTEORDER(3)

NAME
     htonl, htons, ntohl, ntohs -- convert values between host and network
     byte order

要は、ネットワーク上で扱う複数バイト (16bit, 32bit) のバイト順 (Big Endian) と CPU 内部で扱うバイト順 (アーキテクチャーにより Big Endian のものもあれば Little Endian のものもある) を変換するものです。Endian に関しては、Wikipedia エンディアン などを参考にしてください。

同じく if 文の中で使用している V_ipport_reserved{high,low} は、/usr/src/sys/netinet/in_pcb.h (http://svnweb.freebsd.org/base/release/9.1.0/sys/netinet/in_pcb.h?view=markup) で以下のように定義されています。

#define V_ipport_reservedhigh   VNET(ipport_reservedhigh)
#define V_ipport_reservedlow    VNET(ipport_reservedlow)

ipport_reserved{high,low} は、/usr/src/sys/netinet/in_pcb.c (http://svnweb.freebsd.org/base/release/9.1.0/sys/netinet/in_pcb.c?view=markup) で以下のように定義されています。

/*
 * Reserved ports accessible only to root. There are significant
 * security considerations that must be accounted for when changing these,
 * but the security benefits can be great. Please be careful.
 */
VNET_DEFINE(int, ipport_reservedhigh) = IPPORT_RESERVED - 1;    /* 1023 */
VNET_DEFINE(int, ipport_reservedlow);

IPPORT_RESERVED は、/usr/src/sys/netinet/in.h (http://svnweb.freebsd.org/base/release/9.1.0/sys/netinet/in.h?view=markup) で以下のように定義されています。

/*
 * Ports < IPPORT_RESERVED are reserved for
 * privileged processes (e.g. root).         (IP_PORTRANGE_LOW)
 */
#define IPPORT_RESERVED         1024

前述の in_pcb.c の以下の定義は、

VNET_DEFINE(int, ipport_reservedhigh) = IPPORT_RESERVED - 1;    /* 1023 */
VNET_DEFINE(int, ipport_reservedlow);

以下のように展開されます。

int ipport_reservedhigh = 1024 - 1;
int ipport_reservedlow;

ipport_reservedlow には初期値が代入されていませんが、これはカーネル起動時に 0 に初期化されます。IP では、ポート番号 0~1023 までを予約済み番号とし、一般ユーザはこのポート番号を使用できないことになっています。

if 文の三行目の priv_check_cred() は、/usr/src/sys/kern/kern_priv.c (http://svnweb.freebsd.org/base/release/9.1.0/sys/kern/kern_priv.c?view=markup) で以下のように定義されています。

/*
 * Check a credential for privilege.  Lots of good reasons to deny privilege;
 * only a few to grant it.
 */
int
priv_check_cred(struct ucred *cred, int priv, int flags)

元の if 文に戻ります。

if (ntohs(lport) <= V_ipport_reservedhigh &&
    ntohs(lport) >= V_ipport_reservedlow &&
    priv_check_cred(cred, PRIV_NETINET_RESERVEDPORT,
    0))
        return (EACCES);

結局この if 文は、bind(2) システムコールの第二引数 struct sockaddr *addr で指定したポート番号が 0~1023 の場合、root 権限を有するかどうかを判断し、権限のない場合には EACCESS を返却します。

次の処理に進みます。

if (!IN_MULTICAST(ntohl(sin->sin_addr.s_addr)) &&
    priv_check_cred(inp->inp_cred,
    PRIV_NETINET_REUSEPORT, 0) != 0) {
        t = in_pcblookup_local(pcbinfo, sin->sin_addr,
            lport, INPLOOKUP_WILDCARD, cred);

まずは最初の

if (!IN_MULTICAST(ntohl(sin->sin_addr.s_addr)) &&

ですが、IN_MULTICAST() は 前回 説明していますが、sin->sin_addr.s_addr はマルチキャストアドレスではないので、これは真になります。

次の

priv_check_cred(inp->inp_cred,
PRIV_NETINET_REUSEPORT, 0) != 0) {

この priv_check_cred() は、前述の priv_check_cred() です。第二引数の PRIV_NETINET_REUSEPORT は、/usr/src/sys/sys/priv.h (http://svnweb.freebsd.org/base/release/9.1.0/sys/sys/priv.h?view=markup) で以下のように定義されています。

#define PRIV_NETINET_REUSEPORT 504 /* Allow [rapid] port/address reuse. */

これは、setsockopt(2) システムコールで使用する SO_REUSEPORT と関連があるのではないかと推測します。

この先の処理を見ていくと、条件によって

return (EADDRINUSE);

EADDRINUSE を返却しています。現状ではこのエラーは発生していないので、priv_check_cred() およびそれ以降の判定に関しては割愛しますが、bind(2) システムコールの第二引数 struct sockaddr *addr で指定したアドレスが現在使用中か否かを判定しているものと推測します。ちなみに、EADDRINUSE は、bind(2) システムコールを呼び出した際、指定したアドレスまたはポート番号が使用中の場合、このエラーが返却されます。なお、setsockopt(2) システムコールで SO_REUSEADDR あるいは SO_REUSEPORT が設定されていると、このエラーが回避されます。

次の処理に進みます。

t = in_pcblookup_local(pcbinfo, sin->sin_addr,
    lport, lookupflags, cred);
if (t && (t->inp_flags & INP_TIMEWAIT)) {
        /*
         * XXXRW: If an incpb has had its timewait
         * state recycled, we treat the address as
         * being in use (for now).  This is better
         * than a panic, but not desirable.
         */
        tw = intotw(t);
        if (tw == NULL ||
            (reuseport & tw->tw_so_options) == 0)
                return (EADDRINUSE);
} else if (t && (reuseport == 0 ||

in_pcblookup_local() は、in_pcbbind_setup() と同じ (http://svnweb.freebsd.org/base/release/9.1.0/sys/netinet/in_pcb.c?view=markup) で以下のように定義されています。

struct inpcb *
in_pcblookup_local(struct inpcbinfo *pcbinfo, struct in_addr laddr,
    u_short lport, int lookupflags, struct ucred *cred)

in_pcblookup_local() の詳細に関しては割愛しますが、第二引数 struct in_addr laddr と第三引数 u_short lport に該当する struct inpcb へのポインタを返却します。もし該当するものがなければ、NULL を返却します。該当する struct inpcb が存在するということは、そのアドレスおよびポート番号が既に使用されているということです。

なお、struct inpcb に関しては、十一回目 の末尾の図などを参考にしてください。

ここでは in_pcblookup_local() からは NULL が返却されたものとして、

if (t && (t->inp_flags & INP_TIMEWAIT)) {

} else if (t && (reuseport == 0 ||

は偽とみなし、次の処理に進みます。

        if (*lportp != 0)
                lport = *lportp;
        if (lport == 0) {
                error = in_pcb_lport(inp, &laddr, &lport, cred, lookupflags);
                if (error != 0)
                        return (error);

        }
        *laddrp = laddr.s_addr;
        *lportp = lport;
        return (0);
}

lportp には、struct inpcb の inp_lport のポインタが渡されます。struct inpcb は 十一回目 で解説した通り、bzero() にて初期化したのち、まだ inp_lport には何も代入していないので、

if (*lportp != 0)

この if 文は偽となります。

次の if 文ですが、

if (lport == 0) {

前述の通り、lport には 8000 が指定されているものとみなすので、この if文も偽となります。なお、lport が 0 の場合、

if (lport == 0) {
        error = in_pcb_lport(inp, &laddr, &lport, cred, lookupflags);
        if (error != 0)
                return (error);

}

in_pcb_lport() により使用されていないポート番号を割り当てます。

in_pcbbind_setup() の最後の処理です。

*laddrp = laddr.s_addr;
*lportp = lport;
return (0);

laddrp には struct inpcb の inp_laddr.s_addr のポインタが、lportp には同じく struct inpcb の inp_lport のポインタが渡されるので、ここで struct inpcb の各情報が更新されます。inp_laddr と inp_lport は、/usr/src/sys/netinet/in_pcb.h (http://svnweb.freebsd.org/base/release/9.1.0/sys/netinet/in_pcb.h?view=markup) で以下のように定義されています。

#define ie_laddr        ie_dependladdr.ie46_local.ia46_addr4
#define inc_laddr       inc_ie.ie_laddr
#define inp_laddr       inp_inc.inc_laddr

#define inc_lport       inc_ie.ie_lport
#define inp_lport       inp_inc.inc_lport

以下の図ではこのマクロ展開前の簡略した表記にしています。また、*laddrp に代入される値は 0 ですが、分かりやすくするために INADDR_ANY と表記しています。

ここまでのデータ構造を以下に示します。

今回の処理で変更した変数を青、変更した値は赤で示しています。

次回は、in_pcbbind_setup() から in_pcbbind() に戻ったところからです。

著者プロフィール

asou

好きな OS は、FreeBSD です。が、最近購入した Mac mini がなかなか快適なので、Mac でいいかなと思うようになってきました。

記事一覧Index