仙石浩明の日記

stone 開発日記

2006年4月29日

SSL_connect に時間がかかる場合の挙動

epoll版 stone のバグを長いこと放置していた... orz
(1) getident
(2) SSL_connect に時間がかかる場合の挙動

(1) のバグは、sd = socket() するまえに epoll_ctl(epfd, EPOLL_CTL_ADD, sd, &ev) していた、という単純バグ。なぜ epoll_ctl が失敗しなかったというと sd を初期化していなかったので、たまたま有効なディスクリプタだったのだろう。これは順序を入れ替えるだけで fix.

(2) は、少し複雑。stone は中継先に connect する際、どのタイミングで接続が確立するかで処理のパスが変わる。たとえば connect が EINPROGRESS を返すと epoll/select 待ちするが、connect を呼び出した時点で接続が確立すれば、直接 readWrite ループへ遷移する。

SSL がからむともっと複雑になる。SSL_accept が完了するまで中継先が決まらない場合もあるし、TCP接続が確立しても SSL接続がなかなか確立しない場合もある。SSLハンドシェークに時間がかかる場合、SSL_accept / SSL_connect は readWrite ループで呼び出されることになる。

SSL_connect が完了する前に中継元からの入出力が行われてはまずいので、readWrite ループ内の epoll/select 待ちでは中継元側のディスクリプタは監視対象からはずしているわけだが、SSL_connect が成功した時点で監視対象に含めなければならない。select 版では正常に動作するのだが、epoll 版では監視対象に含め損なっていた。

     ret = SSL_connect(ssl);
     if (ret > 0) {     /* success */
+       Pair *p = pair->pair;
        /* pair & dst is connected */
        pair->proto |= (proto_connect | proto_dirty);
+       if (p) p->proto |= proto_dirty; /* src */

接続元側 Pair の proto に、proto_dirty フラグを設定することにより、epoll でも監視対象に含めるようになる。 これで epoll 版も正常に動くようになったはず。
現在 senri で動作検証中...

$Id: stone.c,v 2.2.2.20 2006/04/28 21:50:24 hiroaki_sengoku Exp $

Filed under: stone 開発日記 — hiroaki_sengoku @ 07:44
2006年4月16日

getnameinfo のバグ? (2)

昨日、getnameinfo の問題について述べたが、 念のため、glibc のソースを確認してみた。 手元に展開してあった glibc-2.3.5 を見ると、 glibc-2.3.5/inet/getnameinfo.c の 440行目で、

      case AF_LOCAL:
        strncpy (serv, ((const struct sockaddr_un *) sa)->sun_path, servlen);
        break;

とある。sun_path が 0 終端されていないと、 sun_path が実際に何バイト確保されているかとは関係なく、 servlen バイトがコピーされてしまう。

getnameinfo の salen 引数のチェックは、 同じく getnameinfo.c 182行目の、

    case AF_LOCAL:
      if (addrlen < (socklen_t) (((struct sockaddr_un *) NULL)->sun_path))
        return EAI_FAMILY;
      break;

だけであった。 つまり sun_path の部分が 0 バイトでもエラーにならない。

今回の問題とは関係ないが、

(socklen_t) (((struct sockaddr_un *) NULL)->sun_path))

という書き方は参考になる。 stone.c では

(((struct sockaddr_un*)sa)->sun_path - (char*)sa)

と書いていたのだが、 NULL を使えば簡潔に書ける、 というのは目から鱗だった。

Filed under: stone 開発日記 — hiroaki_sengoku @ 08:17
2006年4月15日

getnameinfo のバグ? (1)

UNIX で TCP接続を accept したとき、そのソケット sd に対して

struct sockaddr_storage ss;
struct sockaddr *from = (struct sockaddr*)&ss;
socklen_t fromlen = sizeof(ss);
getpeername(sd, from, &fromlen);

などとすれば、接続元のアドレスが from に返る。 UNIXドメインソケットを listen し accept した場合は、

from->sa_family ← AF_UNIX
fromlen ← 2

という値が返るようだ。

ところが、
この返り値を getnameinfo に与えると、 fromlen で指定したサイズを超えて UNIX ドメインソケットのファイル名を読もうとする 問題があるようだ。

たとえば次のようなテストプログラムを書いてみる:

#include <stdio.h>
#include <sys/socket.h>
#include <netdb.h>
#include <sys/un.h>
#define STRMAX  256
main() {
    struct sockaddr *sa;
    socklen_t salen;
    struct sockaddr_un sun;
    char name[STRMAX+1];
    char serv[STRMAX+1];
    int err;
    bzero(name, STRMAX+1);
    bzero(serv, STRMAX+1);
    sa = (struct sockaddr*)&sun;
    salen = sizeof(sun);
    sun.sun_family = AF_UNIX;
    strcpy(sun.sun_path, "/tmp/sock");
    salen = sizeof(sa_family_t);
    err = getnameinfo(sa, salen, name, STRMAX, serv, STRMAX, 0);
    printf("salen: %d, err: %d, name: '%s', serv: '%s'\n",
           salen, err, name, serv);
}

salen は 2 であるので、 sun.sun_path の部分の値は無視すべきであるのに、 serv には、"/tmp/sock" の値が返ってしまう。 sun.sun_path の部分が初期化されていなければ、 不定値が返るだろう。

実際、stone では struct sockaddr_storage を初期化せずに getpeername を呼び出して長さ 2 の struct sockaddr_un を受け取り、 これを getnameinfo の引数として与えると、 dserv に不定値が返ってきてしまった。

本来ならば、getnameinfo は長さ 0 のパス名を返すか、 あるいは UNIX ドメインソケットは扱わない、 ないしは長さが異常ということで EAI_FAMILY を返すか、 どちらかであるべきではなかろうか。 少なくとも glibc-2.3.5 と、glibc-2.2.5 では、 この問題があることを確認した。

とりあえず、stone は AF_UNIX のときは getnameinfo を呼ばずに sun_path を dserv へコピーするように修正した。

$Id: stone.c,v 2.2.2.19 2006/04/14 23:35:18 hiroaki_sengoku Exp $

Filed under: stone 開発日記 — hiroaki_sengoku @ 09:58
2006年4月14日

DB サーバのセキュリティ向上策 (6)

DB サーバのセキュリティ向上策 (3)」では、 どのユーザが UNIX ドメインソケットにアクセスしたか特定するために、 ユーザごとにソケットを作成しました。 しかし Linux などの OS では、UNIX ドメインソケットに限り アクセス元のユーザを getsockopt(2) で調べることができるので、 ソケット一つだけでどのユーザからのアクセスか区別することができます。

例えば以下のように getsockopt の引数に SO_PEERCRED を指定することにより、 ucred 構造体にアクセス元のユーザの情報が格納されます:

struct ucred cr;
int len = sizeof(cr);
int ret = getsockopt(sd, SOL_SOCKET, SO_PEERCRED, &cr, &len);
if (ret == 0) {
    struct passwd *passwd = getpwuid(cr.uid);

    ...

}

したがって、UNIX ドメインソケットを listen し、 そのソケットへ接続があったら、 それを DB サーバへ通信を監視しつつ中継するプログラム dbrelay において、 上記のように getsockopt(2) を使ってアクセス元ユーザを特定し、 通信の監視によって得られた DB ユーザ名と一致したら中継を継続し、 一致しなかったら通信を遮断する、ということが可能になります。

もちろん、各ユーザが直接 DB サーバへアクセスしないよう、 dbrelay からのアクセス以外は OUTPUT チェインで通信を却下します。

このような仕掛けを、ラック内の全サーバに仕込むことにより、 各ユーザは同名の DB ユーザでのみ DB サーバへ、 UNIX ドメインソケット経由でアクセスできます。

通信の監視をクライアント側でなく、 DB サーバ側で行ないたい場合は、 クライアント側の dbrelay では アクセス元のユーザ名をサーバ側へ伝えるだけにして、 通信の監視および通信の遮断はサーバ側で行なわせることもできます。

最新版 の stone には アクセス元のユーザ名をサーバ側へ伝える機能があるので、 例えばクライアント側で次のように stone を実行します。

stone dbserver:12345/http /tmp/db.sock '\U'

「/http」の指定と「'\U'」が、ユーザ名をサーバ (dbserver:12345) へ 伝える設定です。例えばユーザ sengoku が UNIX ドメインソケット /tmp/db.sock にアクセスすると、stone はまず

Apr 14 07:54:36.809789 16384 3 5>6 73 65 6e 67 6f 6b 75 0d  sengoku.
Apr 14 07:54:36.809828 16384 3 5>6 0a 0d 0a                 ...

を dbserver:12345 へ送信し、その後は /tmp/db.sock から dbserver:12345 への 中継が行なわれます。 したがってサーバ側ではまず最初に送られてきたユーザ名を受け取り、 その後は通信を監視しつつ 12345 番ポートから DB サーバへの中継を行なえばよいわけです。

Filed under: stone 開発日記,システム構築・運用 — hiroaki_sengoku @ 08:13
2006年4月14日

SO_PEERCRED

UNIX ドメインソケットならば、カーネルはどのユーザが そのソケットに connect したか知っているはずで、 きっと getsockopt(2) でユーザID が取得できるに違いない、 と思ってカーネルソースを見ていたら SO_PEERCRED を発見。

ついでに SO_PEERCRED をググってみたら、 PostgreSQL や w3m では、 既に SO_PEERCRED を使った認証をサポートしていることが判明。 負けじと stone にも実装してみる。

$Id: stone.c,v 2.2.2.18 2006/04/13 09:02:15 hiroaki_sengoku Exp $

README から引用:

        stone [-C <file>] [-P <command>] [-Q <options>] [-N] [-d] [-p] [-n]
              [-u <max>] [-f <n>] [-l] [-L <file>] [-a <file>] [-i <file>]
              [-X <n>] [-T <n>] [-r]
              [-x <port>[,<port>][-<port>]... <xhost>... --]
              [-s <send> <expect>... --]
              [-b [<var>=<val>]... <n> <master>:<port> <backup>:<port>]
              [-B <host>:<port> <host1>:<port1>... --]
              [-I <host>]
              [-o <n>] [-g <n>] [-t <dir>] [-D] [-c <dir>]
              [-q <SSL>] [-z <SSL>]
              [-M install <name>] [-M remove <name>]
              <st> [-- <st>]...

        ...

        <st> は次のいずれかです。<st> は「--」で区切ることにより、複数個
        指定できます。

        ...

        (4)        <host>:<port>/http <request> [<xhost>...]
        (5)        <host>:<port>/proxy <header> [<xhost>...]

        (4) は、http リクエストにのせて中継します。<request> は HTTP 1.0 
        で規定されるリクエストです。リクエスト文字列中、「\」はエスケー
        プ文字であり、次のような置き換えが行なわれます。

                \n        改行 (0x0A)
                \r        復帰 (0x0D)
                \t        タブ (0x09)
                \\        \    (0x5C)
                \a        接続元の IP アドレス
                \A        「接続元の IP アドレス」:「ポート番号」
                \d        接続先の IP アドレス
                \D        「接続先の IP アドレス」:「ポート番号」(透過プロキシ用)
                \u        接続元のユーザID (番号)
                \U        接続元のユーザ名
                \g        接続元のグループID (番号)
                \G        接続元のグループ名
                        \u \U \g \G は UNIX ドメインソケットの場合のみ
                \0        SSL 証明書のシリアル番号
                \1 - \9        SSL オプションの re<n>= で指定した正規表現中、
                        ( ... ) 内の正規表現にマッチした文字列
                \?1<then>\:<else>\/
                        もし \1 (\2 - \9 も同様) の文字列が、空文字列で
                        なければ <then>、空文字列であれば <else>

        (5) は、http リクエストヘッダの先頭に <header> を追加して中継し
        ます。(4) と同様のエスケープを使うことができます。
Filed under: stone 開発日記 — hiroaki_sengoku @ 06:58
2006年4月8日

stone 1.0

stone の最初のバージョン。 11年前 Version 1.0 を出したときは、わずか 278行だった (現在の Version 2.3a では、9100行を超えている)。

/*
 * stone.c        simple repeater
 * Copyright(C)1995 by Hiroaki Sengoku <sengoku@virgo.bekkoame.or.jp>
 * Version 1.0        Jan 28, 1995
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GNU Emacs; see the file COPYING.  If not, write to
 * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * Usage: stone <st> [-- <st>]...
 * <st> := <screen> [<hosts>...] | <host>:<port> <sport> [<hosts>...]
 *
 * (1) Any packets received by <screen> are passed to DISPLAY
 * (2) Any packets received by <sport> are passed to <host>:<port>
 * (3) as long as these packets are sent from <hosts>...
 * (4) if <hosts> are not given, any hosts are welcome.
 */

#include        <stdio.h>
#include        <stdlib.h>
#include        <string.h>
#include        <errno.h>
#include        <sys/types.h>
#include        <sys/time.h>
#include        <sys/socket.h>
#include        <netinet/in.h>
#include        <netdb.h>
#define        BACKLOG_MAX        5

#define XPORT                6000
#define BUFMAX                256

#define STONEMAX        (FD_SETSIZE/3)        /* max # of stones */
typedef struct {
    int sd;        /* socket descriptor to listen */
    struct sockaddr_in sin;        /* destination */
    int nhosts;                        /* # of hosts */
    struct in_addr xhosts[0];        /* hosts permitted to connect */
} Stone;

/* *addrp is permitted to connect to *stonep ? */
int checkXhost(stonep,addrp)
Stone *stonep;
struct in_addr *addrp;
{
    int i;
    if( !stonep->nhosts ) return 1; /* any hosts can access */
    for( i=0; i < stonep->nhosts; i++ ) {
        if( addrp->s_addr == stonep->xhosts[i].s_addr ) return 1;
    }
    return 0;
}

/* *stonep accept connection */
void doaccept(stonep,fdsp,pair)
Stone *stonep;
fd_set *fdsp;
int *pair;
{
    struct sockaddr_in from;
    int nsd, dsd;
    int len;
    len = sizeof(from);
    nsd = accept(stonep->sd,(struct sockaddr*)&from,&len);
#ifdef DEBUG
    printf("Accept: %x port %d ...",
           ntohl((unsigned long)from.sin_addr.s_addr),ntohs(from.sin_port));
#endif
    if( !checkXhost(stonep,&from.sin_addr) ) {
#ifdef DEBUG
        printf("denied.\n");
#endif
        if( nsd >= 0 ) close(nsd);
        return;
    }
#ifdef DEBUG
    printf("accepted.\n");
#endif
    if( nsd < 0 ) {
        if( errno == EINTR ) return;
        fprintf(stderr,"Accept error.\n");
        return;
    }
    if( (dsd=socket(PF_INET,SOCK_STREAM,0)) < 0 ) {
        fprintf(stderr,"Cannot create socket.\n");
        close(nsd);
        return;
    }
    if( connect(dsd,(struct sockaddr*)&stonep->sin,sizeof(stonep->sin)) < 0 ) {
        fprintf(stderr,"Cannot connect socket.\n");
        close(nsd);
        if( dsd >= 0 ) close(dsd);
        return;
    }
    pair[nsd] = dsd;
    pair[dsd] = nsd;
    FD_SET(nsd,fdsp);
    FD_SET(dsd,fdsp);
}

void repeater(nstones,stones)
int nstones;        /* # of stones */
Stone *stones[];
{
    int sdmax;
    int pair[FD_SETSIZE];
    fd_set fds, rfds;
    int width, i;
    char buf[BUFMAX];
    int nbyte;
    sdmax = stones[nstones-1]->sd + 1;        /* sd of last stone + 1 */
    FD_ZERO(&fds);
    for( i=0; i < nstones; i++ ) FD_SET(stones[i]->sd,&fds);
    width = ulimit(4,0);
    for( i=0; i < width; i++ ) pair[i] = -1;
    while( rfds=fds, select(width,&rfds,NULL,NULL,NULL) > 0 ) {
        for( i=0; i < width; i++ ) {
            if( FD_ISSET(i,&rfds) ) {
                if( i < sdmax ) doaccept(stones[nstones-sdmax+i],&fds,pair);
                else if( (nbyte=read(i,buf,BUFMAX)) > 0 )
                    write(pair[i],buf,nbyte);
                else {
#ifdef DEBUG
                    printf("shutdown %d, close %d\n",pair[i],i);
#endif
                    if( pair[i] >= 0 ) {
                        shutdown(pair[i],2);
                        pair[pair[i]] = -1;
                    }
                    pair[i] = -1;
                    close(i);
                    FD_CLR(i,&fds);
                }
            }
        }
    }
}

void host2addr(name,addrp,familyp)
char *name;
struct in_addr *addrp;
short *familyp;
{
    struct hostent *hp;
    if( hp=gethostbyname(name) ) {
        bcopy(hp->h_addr,(char *)addrp,hp->h_length);
        if( familyp ) *familyp = hp->h_addrtype;
    } else if( (addrp->s_addr=inet_addr(name)) != -1 ) {
        if( familyp ) *familyp = AF_INET;
    } else {
        fprintf(stderr,"Unknown host : %s\n",name);
        exit(1);
    }
}

/* make stone */
Stone *mkstone(dhost,dport,port,nhosts,hosts)
char *dhost;        /* destination hostname */
int dport;        /* destination port */
int port;        /* listening port */
int nhosts;        /* # of hosts to permit */
char *hosts[];        /* hosts to permit */
{
    Stone *stonep;
    struct sockaddr_in sin;
    int i;
    stonep = calloc(1,sizeof(Stone)+sizeof(struct in_addr)*nhosts);
    if( !stonep ) {
        fprintf(stderr,"Out of memory.\n");
        exit(1);
    }
    stonep->nhosts = nhosts;
    bzero((char *)&sin,sizeof(sin)); /* clear sin struct */
    sin.sin_family = AF_INET;
    sin.sin_port = htons(port);        /* convert to network byte order */
    host2addr(dhost,&stonep->sin.sin_addr,&stonep->sin.sin_family);
    for( i=0; i < nhosts; i++ ) {
        host2addr(hosts[i],&stonep->xhosts[i],NULL);
#ifdef DEBUG
        printf("permit %x to connecting to %x:%d\n",
               ntohl((unsigned long)stonep->xhosts[i].s_addr),
               ntohl((unsigned long)stonep->sin.sin_addr.s_addr),dport);
#endif
    }
    stonep->sin.sin_port = htons(dport);
    stonep->sd = socket(AF_INET,SOCK_STREAM,0);
    if( stonep->sd < 0 ) {
        fprintf(stderr,"Can't get socket.\n");
        exit(1);
    }
    if( bind(stonep->sd, (struct sockaddr*)&sin, sizeof(sin)) ) {
        fprintf(stderr,"Can't bind.\n");
        exit(1);
    }
    listen(stonep->sd,BACKLOG_MAX);
#ifdef DEBUG
    printf("stone%3d:%s:%d <- %d\n",stonep->sd,dhost,dport,port);
#endif
    return stonep;
}

help(com)
char *com;
{
    fprintf(stderr,
            "Usage: %s <st> [-- <st>]...\n"
            "   st: <screen> [<hosts>...]"
            "| <host>:<port> <port> [<hosts>...]\n"
            ,com);
    exit(1);
}

int getdist(p,portp)
char *p;
int *portp;
{
    while( *p ) {
        if( *p == ':' ) {
            *p++ = '\0';
            *portp = atoi(p);
            return 1;
        }
        p++;
    }
    return 0;
}

main(argc,argv)
int argc;
char *argv[];
{
    int nstones;        /* # of stones */
    Stone *stones[STONEMAX];
    int i, j, k;
    char display[256], *p, *q;
    char *disphost, *host;
    int dispport, port, sport;
    p = getenv("DISPLAY");
    if( p ) {
        strcpy(display,p);
        getdist(display,&dispport);
        disphost = display;
        dispport += XPORT;
    } else {
        disphost = NULL;
    }
    if( argc < 2 ) help(argv[0]);
    setbuf(stdout,NULL);
    nstones = 0;
    for( i=1; i < argc; i++ ) {
        if( getdist(argv[i],&port) ) {
            host = argv[i++];
            if( argc <= i ) help(argv[0]);
            sport = atoi(argv[i++]);
        } else {
            host = disphost;
            port = dispport;
            sport = XPORT+atoi(argv[i++]);
        }
        j = 0;
        k = i;
        for( ; i < argc; i++, j++ ) if( !strcmp(argv[i],"--") ) break;
        stones[nstones++] = mkstone(host,port,sport,j,&argv[k]);
    }
    q = argv[argc-1] + strlen(argv[argc-1]);
    for( p=argv[1]; p < q; p++ ) *p = '\0';
    repeater(nstones,stones);
}
Filed under: stone 開発日記 — hiroaki_sengoku @ 11:45
2006年4月7日

DB サーバのセキュリティ向上策 (5)

前々回(3)前回(4) と、 他人の DB パスワードを知ったユーザが、 その人になりすまして DB にログインしようとしても DBサーバへの通信を遮断する仕掛けについて説明しました。 (3) はクライアント側で、(4) はサーバ側で、 それぞれ DBサーバへの通信を監視し、 UNIX ユーザ名と、DB ユーザ名が一致しない場合は通信を遮断します。

DB サーバへの通信から DB ユーザ名を抽出する方法は、 DB サーバの種類によって異なるので、 専用のものを実装することになるのですが、 今回はその一例として MySQL 4.0.x の場合について説明しましょう。

MySQL の場合は、ソースが公開されていますし、 開発者向けにはプロトコル仕様書もあるようなので、 クライアント-サーバ間通信を完全に解析することも可能なのですが、 DBユーザ名を取り出す程度なら通信内容をダンプするだけでも 取り出し方が推測できます。

まずは手始めに通信内容をダンプしてみましょう。 どんなツールを使ってもよいのですが、 ここでは拙作 stone を使ってみます。 適当なポートで listen し、 それを MySQL サーバへ中継しつつ、 通信内容をダンプするように stone を実行します。 例えば、12345番ポートで listen し、 「-ppp」オプションを指定することによって通信内容をダンプさせ、 ホスト「dbserver」の 3306番ポートへ中継させるためには、 次のように stone を実行します。

stone -ppp dbserver:3306 12345

そして、MySQL のクライアントである mysql コマンドを実行します。 MySQL サーバに接続する代わりに、 stone が listen する 12345番ポートに接続させましょう。

mysql -P12345 -h127.0.0.1 -usengoku -pabcdefg

DB ユーザ名「sengoku」、パスワード「abcdefg」で接続します。 この時、stone の出力は次のようになります:

% stone -ppp dbserver:3306 12345
Apr  7 07:04:45.525293 16384 start (2.3a) [19635]
Apr  7 07:04:45.530733 16384 stone 3: senri.gcd.org:3306 <- 0.0.0.0:12345
Apr  7 07:05:03.405877 16384 stone 3: accepted TCP 5 from 127.0.0.1:37758 mode=3

Apr  7 07:05:03.417693 16384 3 5<6 34 00 00 00 0a 34 2e 30  4....4.0
Apr  7 07:05:03.417785 16384 3 5<6 2e 32 35 2d 73 74 61 6e  .xx-stan
Apr  7 07:05:03.417802 16384 3 5<6 64 61 72 64 2d 6c 6f 67  dard-log
Apr  7 07:05:03.417813 16384 3 5<6 00 e8 7e 00 00 2d 3a 79  ..~..-:y
Apr  7 07:05:03.417823 16384 3 5<6 75 5e 58 7e 31 00 2c 20  u^X~1.,
Apr  7 07:05:03.417833 16384 3 5<6 0c 02 00 00 00 00 00 00  ........
Apr  7 07:05:03.417844 16384 3 5<6 00 00 00 00 00 00 00 00  ........
Apr  7 07:05:03.418337 16384 3 5>6 15 00 00 01 85 24 00 00  .....$..
Apr  7 07:05:03.418374 16384 3 5>6 00 73 65 6e 67 6f 6b 75  .sengoku
Apr  7 07:05:03.418385 16384 3 5>6 00 53 4d 5c 4a 55 53 49  .SM\JUSI
Apr  7 07:05:03.418393 16384 3 5>6 57                       W
Apr  7 07:05:03.418643 16384 3 5<6 05 00 00 02 00 00 00 02  ........
Apr  7 07:05:03.418671 16384 3 5<6 00                       .

各行始めの「Apr 7 07:04:45.525293」は時刻で、マイクロ秒単位で表示しています (マイクロ秒単位まで表示するようになったのは stone 2.3a からです)。 次の「16384」はスレッドID ですが、今回は無視してもらって構いません。 最初の三行は stone のログなので無視するとして、 次の部分が MySQL サーバからクライアントへ送られた通信内容です。 「5<6」が、サーバ→クライアント方向の通信であることを示します。

Apr  7 07:05:03.417693 16384 3 5<6 34 00 00 00 0a 34 2e 30  4....4.0
Apr  7 07:05:03.417785 16384 3 5<6 2e 32 35 2d 73 74 61 6e  .xx-stan
Apr  7 07:05:03.417802 16384 3 5<6 64 61 72 64 2d 6c 6f 67  dard-log
Apr  7 07:05:03.417813 16384 3 5<6 00 e8 7e 00 00 2d 3a 79  ..~..-:y
Apr  7 07:05:03.417823 16384 3 5<6 75 5e 58 7e 31 00 2c 20  u^X~1.,
Apr  7 07:05:03.417833 16384 3 5<6 0c 02 00 00 00 00 00 00  ........
Apr  7 07:05:03.417844 16384 3 5<6 00 00 00 00 00 00 00 00  ........

MySQL のバージョン番号らしきものが送られている様子が分かります。 ここでの目的は DB ユーザ名を抽出することであり、 DB ユーザ名は、クライアント→サーバ方向に送られるはずなので、 サーバ→クライアント方向の通信は無視してしまっても構わないのですが、 先頭の「34 00」には注目しておいた方がよいでしょう。 この手のクライアント-サーバ間プロトコルでは、 先頭にこれから送るデータの長さを送信することが一般的だからです。 実際、「34 00」は十進数で言うと 52 ですが、 クライアントへ送られたデータは 56 バイトなので、 先頭 2 バイトがデータ長、次の 2 バイト「00 00」がデータの種別、 残りの 52 バイトがデータだろう、と推測できます。

続く部分が、クライアントからサーバへ送られた通信内容です。 「5>6」が、クライアント→サーバ方向の通信であることを示します。

Apr  7 07:05:03.418337 16384 3 5>6 15 00 00 01 85 24 00 00  .....$..
Apr  7 07:05:03.418374 16384 3 5>6 00 73 65 6e 67 6f 6b 75  .sengoku
Apr  7 07:05:03.418385 16384 3 5>6 00 53 4d 5c 4a 55 53 49  .SM\JUSI
Apr  7 07:05:03.418393 16384 3 5>6 57                       W

先頭 2 バイト「15 00」がデータ長であると仮定してみます。 十進数で言うと 21 で、 データ種別と推測される 2 バイトデータ「00 01」の後に 21 バイトのデータが続いています。 そして、データの中に明らかに DB ユーザ名と思われる文字列「sengoku」と そのデリミタと思われる「00」があります。 続く 8 バイトはパスワードと推測できます。 DB ユーザ名の前の 5 バイトのデータ「85 24 00 00 00」は、 固定長のデータと推測できるので、 DBユーザ名を抽出するには、 クライアント→サーバ方向のデータのうち、 最初から 10 バイト目から「00」までを取り出せば良さそうです。

もちろん実際に運用する際は、MySQL のソースないしプロトコル仕様書を 参照して確実を期するべきですが、 プロトコル仕様が公式には公開されていない DB サーバの場合でも、

  • DB ユーザ名が取り出せなかった場合は、ログに出力した上で通信を容認
  • DB ユーザ名が取り出せたが、その DB ユーザ名が DB に存在しない場合は、 ログに出力した上で通信を容認
  • DB ユーザ名が取り出せて、かつその DB ユーザ名が存在する場合は、 取り出した DB ユーザ名が正しいものとして扱い、通信の許可/遮断を行なう

などと、DB ユーザ名を正しく取り出せなかったと思われるときは とりあえず通信を容認しておいて、 後ほど DB ユーザ名の抽出方法を改訂する、という運用が可能でしょう。

なお、MySQL の場合は前述したように公式なプロトコル仕様書もありますし、 Ian Redfern 氏がソースを解析して作成したプロトコルの解説が 以下のページで公開されています。

MySQL Protocol (MySQL 3.22 ~ 4.0)
http://www.redferni.uklinux.net/mysql/MySQL-323.html
MySQL Protocol (MySQL 4.1)
http://www.redferni.uklinux.net/mysql/MySQL-Protocol.html
Filed under: stone 開発日記,システム構築・運用 — hiroaki_sengoku @ 09:35
2006年4月4日

DB サーバのセキュリティ向上策 (2)

KLab で採用している DB サーバのセキュリティ向上策を説明する前に、 DB アクセスの際に SSL クライアント認証を行なう方法について説明します。

DB サーバへのアクセスにおいて SSL クライアント認証を必須とし、

(a) 開発者が (デバッグ目的等で) DB アクセスを行なう時は、 その開発者個人が管理する SSL 秘密鍵を用いてアクセスするようにし、

(b) プログラムが DB アクセスを行なう時は、 そのプログラムの実行権限でのみアクセスできる SSL 秘密鍵を 用いるようにすれば、

普通のパスワード認証よりはセキュリティを向上させることができます。 SSL クライアント認証をサポートしている DB サーバであれば、 その機能を有効にするだけで良いので、とても手軽な方法です。

ここで注意したいのは、 普通に SSL を使う (つまり SSL による暗号化のみを行なう、 あるいは暗号化に加えて SSL サーバ認証のみ行なう) ことは、 あまり意味がない、ということです。

データセンタのラック内の通信の盗聴を心配しても仕方がないわけで、 通信路を暗号化すること自体には意味はないでしょう。 また、サーバ認証は接続先が意図したサーバか確認するための手段ですが、 これもラック内で、通信相手が意図通りのサーバでないか心配しても 仕方がないですね。

SSL クライアント認証をサポートしていない DB サーバでも、 拙作 stone を使えば、手軽に SSL クライアント認証を付け加えることができます。

例えば DB サーバへのアクセスを unix ドメインソケット /path/to/socket でのみ 受け付けている場合、このソケットへのアクセス権限を DB サーバの実行ユーザ (ここでは uid が 1001 番とします) 権限に限定して、 local ユーザがアクセスできないようにしておいて、 stone を次のように実行します。

stone -o 1001 -z verify \
      -z CApath=/usr/local/ssl/certs \
      -z key=/usr/local/ssl/private/db.pem \
      /path/to/socket 12345/ssl

「-z verify」が SSL クライアント認証を必須とするためのオプションです。 「-z CApath=/usr/local/ssl/certs」オプションで、 CA の証明書を置いてあるパスを指定します。

すると、その CA が署名した SSL 証明書に対応する秘密鍵を 持っているクライアントが、その SSL 証明書を提示して ポート 12345 番に SSL 接続してきた時のみ、 stone はそれを /path/to/socket へ中継します。

CA が署名した SSL 証明書ならなんでも OK とするのではなく、 特定の CA の特定の証明書のみを受け付ける場合は、

stone -o 1001 -z verify \
      -z CApath=/usr/local/ssl/certs \
      -z key=/usr/local/ssl/private/db.pem \
      -z depth=1 \
      -z re1='/CN=KLAB Root CA[/$]' \
      -z re0='/CN=dbuser[0-9]*[/$]' \
      /path/to/socket 12345/ssl

などのように、SSL証明書の発行対象の名称 (CN) を特定するための 正規表現 (この例では「/CN=dbuser[0-9]*[/$]」) を指定します。

なお、ここでいう証明書は、いわゆる「オレオレ証明書」で構いません。 つまり DB サーバの管理者が必要に応じて何枚でも独自に発行できます。

「オレオレ証明書」が問題となるのは、認証する側が認めていない CA が 証明書を発行する場合です。 例えばサーバ認証を行なう場合は、認証する側は WWW ブラウザなどになります。 WWW ブラウザのユーザが認めていない CA を勝手に立てて証明書を発行し、 ユーザにきちんと説明することなくその証明書を受け入れることを 強要すべきではありません。これがいわゆる「オレオレ証明書」です。

一方、クライアント認証の場合は、認証を行なうのはサーバ側であるので、 サーバの管理者が認める CA ならばなんでも構いません。 もちろんサーバの管理者が独自に立てた CA で OK です。

上記の例では、KLab で立てた「KLAB Root CA」が発行した証明書で、 かつその発行対象の名称 (CN) が「dbuser」+ 0桁以上の数字 の場合 のみ接続を受け付けます。

DB へアクセスする必要がある開発者と、DB へアクセスするプログラム それぞれに別々の証明書を発行すれば、DB 側で 誰からのアクセスか特定することが可能になります。

しかしながら、

(1) プログラム用の秘密鍵を誰が管理するのか?

(2) SSL クライアント認証にともなう負荷増大

という問題が残ります。(2) はコストを度外視すれば済むので、 高いパフォーマンスが要求されない場合は問題とならないかも知れません。 が、(1) はなかなかやっかいです。

プログラムの開発者は、プログラムの実行権限で読むことができるファイルは 何でも読めます。例えば、秘密鍵を読んで特定の場所へコピーするよう プログラムを改変することは至って簡単でしょう。 したがって、プログラム専用の秘密鍵というのは実は 開発者全員で共同管理している秘密鍵と大差ありません。

プログラムの開発者なら誰でも DB にアクセスできる、 という状況を許容するなら、わざわざクライアント認証のような 負荷の高い方法を選択しなくても、と思うのが人情です。

ではどういう方法がよいでしょうか?

(続きは次回に)

Filed under: stone 開発日記,システム構築・運用 — hiroaki_sengoku @ 08:03
2006年4月2日

stone 2.3a (候補) ベンチマーク

select でも性能低下を招かないように修正した stone のベンチマーク結果。

req/sec ms/req KB/sec
select 681.83 1.467 191.59
epoll 692.74 1.444 194.66
apache 983.60 1.017 278.36

select 版と epoll 版で、ほぼ同等の速度。もちろん、listen するポート数が多くなれば差が出てくる可能性はある。

測定条件:

$Id: stone.c,v 2.2.2.15 2006/04/02 00:03:56 hiroaki_sengoku Exp $
senri% stone -rn localhost:80 2345 >& /dev/null
asao% ab -n 1000 -c 10 http://senri:2345/health
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.4 $> apache-2.0
Document Path:          /health
Document Length:        131 bytes
Concurrency Level:      10
req/sec
Requests per second [#/sec] (mean)
ms/req
Time per request [ms] (mean, across all concurrent requests)
KB/sec
Transfer rate [Kbytes/sec] received
Filed under: stone 開発日記 — hiroaki_sengoku @ 09:16
2006年4月1日

stone 2.3 が遅い理由

epoll 化にあたって stone 2.3 のコードの全面的な見直しを 行っていたところ、性能低下を引き起こす問題点を発見。

stone 2.3 において、TCP 接続を accept し、 中継先へ connect する部分の構造は、 次のようになっている(説明のため大幅に単純化している)。

fd_set rin;

main() {
    ....
    for (;;) repeater();
}

void repeater(void) {
    ....
    rout = rin;
    ret = select(FD_SETSIZE, &rout, &wout, &eout, timeout);
    ....
    if (FD_ISSET(stone->sd, &rout)) {
        FD_CLR(stone->sd, &rin);
        ....
        ASYNC(asyncAccept, stone);
    }
}

void asyncAccept(Stone *stone) {
    ....
    nsd = accept(stonep->sd, from, &fromlen);
    FD_SET(stonep->sd, &rin);
    ....
}

つまり、select(2) によって listen しているポート (stone->sd) が accept 可能であることが判明すると、 まず rin の該当ビットをクリア(FD_CLR)し、 accept 処理を行う子スレッドを立てる(asyncAccept)。 このスレッドは accept(2) を呼び出し、 rin の該当ビットを再セット(FD_SET)する。

ところが、子スレッドと親スレッドのどちらが先に処理されるか、 という問題がある。 子スレッドが先に実行されるなら、 rin の該当ビットが再セットされた後、 親スレッドの select(2) が実行されるので問題は起きないが、 普通のスレッド実装では、 親スレッドの実行が優先されるようだ。

つまり、親スレッドの処理が先に実行されると、 rin の該当ビットがクリアされたままで、 親スレッドが select に突入してしまう。 親スレッドが select 待ちになってから、 子スレッドが rin を再セットするが、 時すでに遅し、 親スレッドが呼び出した select では、 listen しているポート (stone->sd) は 監視しない状態になってしまっている。

このため、この select が timeout して、 次回の select 待ちに入るまで、 接続を受け付けない状態が続いてしまう。 timeout は 0.1秒なので、 秒あたり高々10回の接続しか受け付けられなくなってしまう。

この問題を回避するには、 子スレッドで accept するのではなく、 親スレッドで accept すればよい。 この場合、rin の該当ビットをクリアする必要もなくなる。

で、なぜ epoll 化したことによって高速化されたかというと、 epoll の場合、rin に相当するものがカーネル空間にあるわけで、 子スレッドで EPOLL_CTL_MOD (FD_SET に相当) すると、 即それが epoll_wait 待ち (select 待ちに相当) している 親スレッドに反映するためだろう。

Filed under: stone 開発日記 — hiroaki_sengoku @ 18:37
2006年3月26日

epoll(3)

epoll 対応によって高速化した stone の新バージョンのベンチマーク。

version req/sec ms/req KB/sec
stone 2.3 (select) 9.96 100.394 2.80
stone 2.3a (epoll) 781.41 1.280 219.58
apache 983.60 1.017 278.36
stone 2.3
stone 2.3 公式リリース版
stone 2.3a
現時点では CVS にのみ登録されている、次のバージョン:
$Id: stone.c,v 2.2.2.12 2006/03/26 07:30:20 hiroaki_sengoku Exp $
apache
stone なしで直接 httpd へアクセスして測定。

select 版に比べ、epoll 版は、転送速度で 100倍近い性能。 apache への直接アクセスに比べたオーバヘッドは、27% 程度。

# 追記: 性能向上の理由は epoll 化とは別のところにあったことが後日判明

測定条件:

senri% stone -rn localhost:80 2345 >& /dev/null
asao% ab -n 1000 -c 10 http://senri:2345/health
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.4 $> apache-2.0
Document Path:          /health
Document Length:        131 bytes
Concurrency Level:      10
req/sec
Requests per second [#/sec] (mean)
ms/req
Time per request [ms] (mean, across all concurrent requests)
KB/sec
Transfer rate [Kbytes/sec] received
Filed under: stone 開発日記 — hiroaki_sengoku @ 20:29
2006年3月24日

epoll(2)

stone (従来は select を使用) の epoll 対応の続き。

typedef struct _Pair {
  int common;
  struct _Pair *pair;
  ...
  SOCKET sd;  /* socket descriptor */
} Pair;
...
 struct epoll_event ev;
 ev.events = EPOLLONESHOT;
 ev.data.ptr = pair;
 epoll_ctl(ePollFd, EPOLL_CTL_ADD, pair->sd, &ev);

といった感じで、 ePollFd にソケットディスクリプタを Pair 構造体と一緒に登録する。 そして、epoll_wait でイベント発生を待つ。

 struct epoll_event evs[EVSMAX];
 ...
 ret = epoll_wait(ePollFd, evs, EVSMAX, timeout);

配列 evs には、 発生したイベントが epoll_ctl で登録した構造体とともに 格納されるはずだが、

 common = *(int*)evs[i].data.ptr;

などと構造体のデータを取り出そうとすると、 Segmentation violation が発生することがある。 しかも、デバッグ環境では正常に動くのに、 GCD のゲートウェイ上の設定環境で動かすと、 数分程度で Segmentation violation が発生してしまう。

なぜ期待したデータが得られないのかと、 stone のソースコード全体を見直すことに... しかし問題は見つからず。 epoll の仕様に見落としている点があるのかと思って マニュアルを読み直したり、 挙句の果てに、 Linux のカーネルの epoll 周辺のソースコードをながめたりした。

ptr の値が期待したものと異なる場合があるのかと思って、 epoll_ctl で登録したポインタと、 epoll_wait で取り出したポインタの値を表示させてみると、 予想に反してポインタのアドレス自体は正常のようだ。

早朝、このような stone のデバッグをやっていたのだが、 出社の時間が迫っていたので、作業を中断して出かける。

More...
Filed under: stone 開発日記 — hiroaki_sengoku @ 07:15
2006年3月12日

epoll(1)

select(2) は、/usr/include/bits/typesizes.h に

#define __FD_SETSIZE 1024

などと書いてあって、 1024個以上の socket を扱えない。 poll(2) を使えばいいのだが、 Linux で使えれば十分という気もするので、 epoll(2) を使ってみることにする。 土曜の晩と、日曜の早朝をつかって一気にコーディング。

エラー処理など、細かい点はまだだが、とりあえず動いているようだ。 udp 中継は、格段に速くなる。 tcp 中継は、もともとセッションごとに thread 立てて 2socket だけで select していたので、オーバヘッドは大きくない。 epoll 化してもあまり速度は変わらない。

Filed under: stone 開発日記 — hiroaki_sengoku @ 22:30
2006年3月10日

stone のデバッグ

久しぶりの stone デバッグ。

SSL_accept に時間がかかり、 かつ SSL_accept に失敗するときのみ、 中継先への socket を閉じないというバグ。

localhost でテストをしている限り発覚しないし、 lsof などを使って閉じていない socket を見ない限り なかなか気づきにくい。

$Id: stone.c,v 2.2.1.8 2006/03/09 10:13:02 hiroaki_sengoku Exp $

Filed under: stone 開発日記 — hiroaki_sengoku @ 08:35
« Newer Posts