仙石浩明の日記

2006年4月9日

好きなことをしよう

向いていることを仕事にしよう。 まさに、好きこそものの上手なれ、 嫌々やってる人と、好きでやっている人とでは、どんどん差がつく。 一生のうちのかなりの時間を仕事に費やすのだから好きなことをすべき。

好きなことやっててメシが食えれば苦労はしない、という人が いるかも知れない。しかし、 かなりニッチな分野でも、トップクラスならば飯の種には困らないもの。 ベンチャーと同じ。 市場規模が小さくても、他社に負けない競争力を持っていれば 商売になる。 市場規模がいくら大きくても、他社の後追いなら 価格競争に巻き込まれてジリ貧になる。 まさに、鶏口となるも牛後となるなかれ。

その一方で、

眠る開発屋blog から引用:

ベンチャーって割と志先行になりがちだけども、 どこかでそれと距離を置かないと行き詰まるよね。 多種多様な人間を巻き込む土壌がなければバランス悪くなるもの。 ってか事業を展開させようにも志だけじゃ人が集まらない。

確かにそういう面も否定できない。 多種多様な人間を巻き込みつつ、 各人が高い志を持ち続けるには? ベクトルが異なる志を持つ人達が、 お互いを尊重して総合力を発揮するには?

志まで雇われたいの? から引用:

会社を設立するに当っては、この志というものは欠かせない。 しかしそれは創立者や役員といった「コアメンバー」で押しとどめるべきだ。 会社は従業員に雇用契約以上のものを求めてはならないし、 従業員もまた会社に雇用契約以上のものを求めるべきではない。
...
しかし、セミナーだの訓示だので、志を植え付けるがごとくは、 会社の傲慢というものだ。それでよかった時代もあったのかも知れないが、 今や社畜という生き物は不良資産扱いだ。企業不祥事の際に 産廃扱いで捨てられるのがヲチである。
会社2.0に取りかかる前に、せめて会社1.0をリリースしませんか、みなさん。

志を押しつけ、異なるベクトルを矯正しようとするのではなく、 異なるベクトルの合力を生かす方法はないのだろうか? 力をあわせる唯一の目標が「皆でお金持ちになろうね」(眠る開発屋blog) だけだったとしたら、あまりに悲しい。

Filed under: 技術者の成長 — hiroaki_sengoku @ 08:43
2006年4月8日

マーケティングを学ぼう

先日、KLab のエンジニアの T さんが、 KLab 社内 ML でマーケティングの本を紹介しました。

今週のマーケティング本:「マーケティング / 恩蔵直人
私がマーケティングを学んだときの師から紹介された本。 入門書として読むのも良いですが、網羅的かつ体系的に纏められており、 マーケ脳を整理・刺激するのにも適していると思います。 電車の中でも読みやすい文庫サイズ。

さっそく買って読んでみました。 網羅的に分かりやすく説明してあるので、 マーケティングを専門としない人にもいい本ですね。 そこで、私も推薦文を tech ML (KLab 技術者 ML) に書きました:

- o -

T さんオススメのマーケ本を買いました。 この本に書いてある事くらいは、 きちんと押えておくと、 技術者以外の人と話す時に役立つのではないかと思います。

つまり、技術者、企画者、マーケター、セールス、 会社ではいろんな人が業務を分担して仕事を進めるわけですが、 共通言語を持っていないと意思疎通がうまく いかなくて仕事がスムーズに進みませんよね?

で、共通言語というかバックグラウンドを少しでも共有するために、 お互いの分野の基礎的な部分くらいは、網羅的に押えておく事が重要ですね。

なので、技術者こそ、こういうマーケティングの基礎を網羅的に解説した本を 読むべきだと思います。 後ほど本棚に置いておきますので、是非読んで下さい。

# 真っ先に読みたい、という人は私が本棚に置く前に取りに来てね。

- o -

というメールを tech ML に流した直後、 KLabセキュリティの Windows プログラミングが得意な S さんが取りに来ました。 彼は日頃、マーケティングな人達から無理難題 ;) を押しつけられて 困っているので、 マーケティングの基礎を学ぶことはきっと役にたつことでしょう。

こういう本を読んで、 マーケティングとはそもそもどんなことなのか知っておかないと、 同じプロジェクトで一緒に働いているマーケ担当な人が、 マーケタとしてどのくらい優秀な人なのか、 あるいはダメな人か分かりませんよね?

同僚が優秀なマーケタなら、 その人の言う事を信じて開発方針を決めてかまわないのですが、 そうでないマーケタの場合は、 言う事を鵜呑みにするのは危険ですよね? 信じる者は足をすくわれてしまうでしょう。 そういう場合は、鵜呑みにせずに、 他の人の意見を聞くなり、自分で考えるなりしなくてはなりません。

あるいは立場を逆にして考えてみると分かりやすいかも知れません。

例えば、技術の事が全く分からない人が、 技術者に開発を依頼をするとき、 その技術者がどのくらい優秀な人か分からないと、 その人が週間で開発できる、 という言葉を信じていいのか良くないのか判断できませんよね? できる、と言ったから信じたのに~、と言っても後の祭です。

だから技術者でない人も、 相手がどのくらい優秀な技術者か判断できる程度には、 技術の基本を網羅的に押えている必要がありますし、 その逆に、マーケティングの専門家でない人も、 相手がどのくらい優秀なマーケターか判断できる程度には、 マーケティングの基本を網羅的に押えている必要がある、 ってことだと思います。

Filed under: 元CTO の日記,技術と経営 — hiroaki_sengoku @ 18:07
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日

adsl-stop

今朝未明、bフレッツのメンテナンス工事があって、 GCDの対外線が切れた。 切れたこと自体は事前に NTT からアナウンスがあったことだし 問題はないのだが、

bフレッツが切れたことにより PPPoE を行っている pppd が終了すると、 /etc/ppp/adsl-lost が実行される。 GCD の adsl-lost は次のようになっている:

#!/bin/sh
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/sbin:/usr/local/bin
. /etc/rc.d/hostname
MASTER_FILE="/var/run/keepalived_vrrp.MASTER"
if [ -f $MASTER_FILE -a "$1" = "/etc/ppp/pppoe.conf" ]; then
    rm -f $MASTER_FILE
    ERROR_FILE="/var/run/keepalived_vrrp.ERROR"
    echo 2 >> $ERROR_FILE
    /command/svc -t /service/keepalived/
fi

つまり、/var/run/keepalived_vrrp.MASTER を削除したうえで keepalived を再起動する。 すると、keepalived は BACKUP 状態に移行する。 すると、

/etc/rc.d/vrrp_notify INSTANCE VI BACKUP

実行されて、vrrp_notify が adsl-stop を呼び出す。

そして、adsl-stop が adsl-connect が強制終了する。 こうして完全な BACKUP 状態になるはずだった。

ところが

今朝起きて見てみると、 ゲートウェイが両方とも BACKUP 状態になっていた。(*_*)

片方のゲートウェイは PPPoE がつながっていて、 インターネットに正常に接続しているのだが、 あいにく BACKUP 状態なので、 GCD LAN のデフォルトゲートウェイアドレスである 192.168.1.1 を 持っておらず、 GCD LAN の他のマシンからインターネットへ接続できない状態に 陥っていた。

どうしてこんな事態になってしまったのか考えるより、 まずは復旧が優先ということで、 PPPoE がつながっているのに BACKUP 状態となっている 中途半端なゲートウェイ上で adsl-connect プロセスを殺し、 keepalived を再起動させて、 完全な BACKUP 状態へ移行させた。

すると、もう片方のゲートウェイが自動的に MASTER へ昇格して、 無事正常な状態に戻った。

その後、原因を調べていると、 前述したように adsl-stop が adsl-connect を強制終了させることが できずに adsl-connect が走り続けていたことが原因と判明。 つまり、adsl-connect が走り続けたために、 BACKUP 状態であるにもかかわらず PPPoE のリトライを行っていた。 そして bフレッツのメンテナンス工事の終了にともなって PPPoE が成功、 BACKUP 状態なのに PPPoE でつながっている、という状態が発生した。 この状態では、正常にインターネットにつながっているために、 MASTER に昇格することはなく、BACKUP 状態が継続する。

一方、もう片方のゲートウェイは、MASTER に昇格しようとして PPPoE を試みても失敗してしまう。 つまり MASTER へ昇格できない。 したがって、両方のゲートウェイが BACKUP 状態のままになる。

というわけで問題は adsl-stop が adsl-connect を 終了させることができない点にありそうだと推測。 あらためて adsl-stop を読みなおすと...

    # Kill pppd, which should in turn kill pppoe
    if [ -r "$PPPD_PIDFILE" ] ; then
        PPPD_PID=`cat "$PPPD_PIDFILE"`
        $LOGGER -p daemon.notice "Killing pppd"
        echo "Killing pppd ($PPPD_PID)"
        kill $PPPD_PID > /dev/null 2>&1 || exit 1
    fi
    ....
    # Kill adsl-connect
    $LOGGER -p daemon.notice "Killing adsl-connect"
    echo "Killing adsl-connect ($PID)"
    kill $PID > /dev/null 2>&1

以前このスクリプトを読んだときは見落としていたのだが、 「kill $PPPD_PID」の後に「|| exit 1」がついている! なんだこれは!?

つまり、pppd プロセスが動いていなければ exit してしまうので、 adsl-connect を終了させないではないか! 誰だ、こんな頭悪いコードを書いた奴は。

adsl-connect は pppd が失敗すると、 リトライするまえに 5秒 sleep する。 つまりこの 5秒間は pppd プロセスが無い。 だから、運悪く sleep 中に adsl-stop を実行すると、 adsl-connect が強制終了されずに残ってしまう。

速攻で adsl-stop から「|| exit 1」の部分を削除した。 これで安心 (だといいな)。

Filed under: システム構築・運用 — hiroaki_sengoku @ 11:56
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月6日

GCDバックアップ回線の監視(3)

GCDバックアップ回線用のフレッツADSL では、 バックアップ回線ということで固定IPアドレスにはしていない。 どのくらい安定してつながっているか過去 10日間の統計を取ってみた。

毎分 ssh のテストログイン 14400回(=10*24*60)の結果:

エラー回数 112回
IPアドレスの切換回数 114回
IPアドレス平均持続時間 123.6分
持続時間の標準偏差(σ) 201.2分
持続時間の中央値 39分
最短持続時間 1分
最長持続時間 1385分

思ったより IPアドレスの切換が激しい。 一時間も持続しないIPアドレスが半数以上にのぼる。 10時間以上安定していたのは 4例だけ。

Filed under: システム構築・運用 — hiroaki_sengoku @ 19:48
2006年4月6日

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

前回は、 DB サーバへの通信を監視して、 UNIX でのユーザ権限と DB ユーザ名が一致する時に限り 通信を許可する方法について説明しました。

アクセス元となるクライアント側のマシンならば、 どのユーザがアクセスしているか容易に調べられるので、 通信の監視もアクセス元で行なった方が簡単です。

しかしながらこの方法では、 データセンタ内の全てのサーバで通信の監視を行わなければならず、 やや繁雑です。 また、UNIX でのユーザ権限ごとのアクセスコントロールを 実現する方法として、 前回は各ユーザごとに専用の UNIX ドメインソケットを用意し、 そのパーミッション設定でコントロールする方法を紹介しましたが、 当然のことながら DB アクセスを行うクライアントプログラムが UNIX ドメインソケットをサポートしていなければなりません。

DB サーバへの通信の監視をアクセス先、 つまり DB サーバー側で行うことができれば、 クライアント側は監視が行われていることを意識する必要がなくなります。 つまり前回説明した「入口規制」によるアクセス制限ですね。 クライアント側から見れば、 普通に DBサーバへ TCP 接続するだけなので、 どのようなクライアントプログラムでも利用可能でしょう。

この入口規制を実現するには、 どのユーザ権限でクライアントプログラムが動いているのか、 DB サーバ側から調べる必要があります。 しかしながら、TCP 接続ではサーバ側は送られてきた パケットを受け取るだけなので、 そのパケットを誰が送信したかまでは関知しません。 そこで、通信相手が誰なのか調べる方法を定めたのが、 RFC1413 で提案された IDENT プロトコルです。 IDENT プロトコルは、 受け取った TCP パケットに記録されている 送信元の IP アドレスとポート番号を手がかりに、 相手が誰かクライアント側へ問い合わせます。

IDENT プロトコルは、あまり使われなくなって久しいので、 まず簡単に紹介しましょう。

ホスト CLIENT からホスト SERVER の 12345番ポートへアクセスした 場合について考えます。 その接続状況は次のように netstat コマンドなどで確認できて、

SERVER % netstat
Proto Recv-Q Send-Q  Local Address  Foreign Address  State
tcp        0      0  SERVER:12345   CLIENT:45486     ESTABLISHED

この場合だと、CLIENT の 45486番ポートから SERVER の 12345番ポートへの TCP 接続が確立している (ESTABLISHED) ことが分かります。

このとき、SERVER は CLIENT に IDENT 問合せをすることによって、 CLIENT のどのユーザが TCP 接続の相手か調べることができます。

SERVER % telnet CLIENT 113
Connected to CLIENT.
45486,12345                                 … (1)
45486 , 12345 : USERID : OTHER :sengoku     … (2)
Connection closed by foreign host.

(1) が SERVER から CLIENT への問合せで、 送受信双方のポート番号の対を送信しています。 これに対し、(2) が CLIENT から SERVER への応答で、 TCP 接続の相手が「sengoku」であることが分かりました。

このように、通信相手が誰か手軽に確認できる便利なプロトコルなのですが、 相手ホストが信用できなければ当然 IDENT 応答も信じることはできません。 不特定多数のホストからの通信を受け付ける場合は、 IDENT 問合せを行なっても「正直な」応答が返ってくるとは期待できないわけで、 少なくともインターネット上ではあまり使われなくなったようです。

しかし データセンタのラック内のサーバであれば、 当然管理者が決まっているでしょうから、 全てのサーバで正しく IDENT 応答を返すように徹底することは容易です。 したがって、 IDENT プロトコルを使った入口規制を実施することができます。

まず DB サーバを、UNIX ドメインソケットでのみアクセスを受け付ける ように設定します。 このソケットは、local ユーザが勝手にアクセスしたりしないよう、 DB サーバを実行するユーザ権限でのみ読み書きできるように 設定しておくとよいでしょう。

次に、特定のポートで listen し、接続を受け付けたら この UNIX ドメインソケットへ中継するプログラムを、 DB サーバを実行するユーザと同じユーザ権限で走らせます。 この中継プログラムは、 中継する際に接続元ホストに対して IDENT 問合せを行ない、 IDENT 応答で得たユーザ名と、 通信を監視することによって得られた DB ユーザ名が 一致しない場合は通信を遮断するというわけです。

このような仕掛けにより、 クライアント側のユーザは、 仕掛けを意識することなく DB サーバにアクセスすることができます。 そして、 何かのはずみに別の DBユーザのパスワードを知り、 その DBユーザになりすましてアクセスしようとしても 通信は遮断されます。

Filed under: システム構築・運用 — hiroaki_sengoku @ 12:25
2006年4月5日

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

一般に、マシン C からマシン S へのアクセスを制限するには、

(IN) マシン S でアクセスを受け付けるところで制限する「入口規制」
(OUT) マシン C でアクセスを出すところで制限する「出口規制」

の2通りの方法があります。一般にアクセス制限というと (IN) の入口規制を用いることが多いのですが、 これはアクセスを出すマシンが特定できないか、 あるいは特定はできるのだけどマシンの数ないし種類が多過ぎるためです。

例えば、マシン S へのログインを制限したい場合を考えてみましょう。 一般的にはアクセス元となりうるマシンは特定できないか、 特定できたとしても数が多いでしょうし、 そのマシンで動く OS の種類も多種多様でしょう。 アクセス元となりうるマシンの全てで、 アクセスを出すことを制限しようとするのは非現実的です。 なので普通はアクセスを受け付ける側のマシン S で クライアントの認証を行なってアクセス制限をします。

例えば ssh の場合ならどこのマシンからのアクセスであっても、 秘密鍵を持っていることを証明できるならアクセスを許可し、 そうでなければ却下します。 アクセスを出す側のマシンでは何の出口規制もなく、 アクセスを受け付ける側のマシンでの入口規制だけですね。

ところが、データセンタのラック内のマシンに限定すれば、 事情は変わってきます。 ラック内に入れてあるマシンの数は高々有限 ;) ですし、 多くの場合全てのマシンで同じ OS が動いているでしょうし、 管理の都合からいえば全てのマシンを同じチームが管理すべきでしょう。

このような特殊な環境下では、 (OUT) の出口規制も現実的な方法となります。 例えば DB サーバへのアクセスを考えると、 DB サーバは通常のサーバ認証のみであっても、 クライアント側でアクセスを出すのを制限する出口規制を設けることによって、 セキュリティを向上させることができます。

クライアント側の OS が Linux であれば、 出口規制の方法として iptables の OUTPUT チェインを使うことができます。 例えば DB サーバが 12345番ポートでアクセスを受け付けているならば、 ラック内の全マシンにおいて

iptables -N dbaccess 2>/dev/null || iptables -F dbaccess
iptables -A dbaccess -j ACCEPT -m owner --uid-owner dbuser  …(2)
iptables -A dbaccess -j DROP                                …(3)

iptables -A OUTPUT -j dbaccess -p tcp --dport 12345 --syn   …(1)

を実行しておきます。

DB サーバへアクセスを出そうとして、 DB サーバが動いているマシンの 12345番ポートへの接続しようとすると、 まず OUTPUT チェインの (1) のルールによって dbaccess チェインへジャンプします。 アクセスを出そうとしたユーザが dbuser であれば、 dbaccess チェインの (2) のルールによって通信が許可されますが、 dbuser 以外のユーザであれば、 (3) のルールによって通信が却下されます。

つまり、dbuser 以外のユーザは、たとえ DB のパスワードを知っていても、 DB にアクセスできない、というわけです。

DB にアクセスしたいユーザが増えてくると、 そのたびに iptables の設定を変更するのは面倒ですので、 DB サーバへ接続できるユーザは一つだけ (例えば dbuser) にしておいて、 そのユーザ権限で中継プログラムを動かすようにすると便利でしょう。

例えば、DB にアクセスしたいユーザ user1, user2, user3, user4 が あるとします。 まず、DB 上に同名のユーザアカウントを作ります。 次に、ラック内の全サーバにおいて、 ユーザ毎に UNIX ドメインソケットを listen し、 そのソケットへ接続があったら、 それを DB サーバへ中継するプログラム (ここでは dbrelay と呼ぶことにしましょう) を動かします。 例えば次のようなソケットをオープンすることになるでしょう。

/home/user1/socket
/home/user2/socket
/home/user3/socket
/home/user4/socket

各ソケット (あるいはソケットを置いてあるディレクトリ) は、 自分以外のユーザは読み書きできないように パーミッションを設定します。 つまり dbrelay は root 権限で起動する必要があります。 さらに、前述したように DB サーバへアクセスするには dbuser ユーザ権限で実行する必要がありますから、 各ソケットをオープンした後は、 dbuser ユーザ権限に setuid する必要があります。

そして、ここが肝なのですが、 dbrelay は DB サーバへの通信を監視し、 接続を受け付けたソケットのユーザ名と、 DB へのアクセスを行なう DB ユーザ名が一致しない場合は 通信を遮断します。

以上のような仕掛けにより、 例えばユーザ user1 は、 /home/user1/socket へ接続することによって DB サーバへアクセスできるが、 そのとき使用できる DB ユーザ名は user1 だけで、 それ以外のユーザ名で DB へアクセスしようとしても dbrelay によって 通信を遮断されることになります。 しかも /home/user1/socket は user1 以外は読み書きできません。 つまり、各ユーザは自身のユーザ名以外では (仮に他の DB ユーザのパスワードを知っていたとしても) DB へアクセスできない ということになります。

Filed under: システム構築・運用 — hiroaki_sengoku @ 08:39
2006年4月5日

人の上に立とう (2) hatena_b

日経ソフトウェアの大森敏行氏の記事中の注釈に、

プログラマが「プログラミングに特別な能力が必要であること」を 強調したがるのには理由がある。 現在の日本のソフトウエア業界には 「プログラミングをしない人のほうが立場が上である」というケースが 往々にして見られ, そのせいでプログラムの質が上がらないという構造的な問題があるからだ。」

というくだりがある。

確かに一理あるし、 優秀なプログラマがエキスパートとして尊敬される土壌づくりは、 優れたソフトウェアを開発するために必須の条件であろう。 しかしながら、 「プログラミングをしない人のほうが立場が上」というのは 一面的な見方ではないのか。 プログラマ兼 CTO である私としては一言いいたくなる。;-)

「プログラミングをしない人」⇒「立場が上」

なのではなくて、

「プログラミングをする人」⇒「視野が狭い人が多い」
「視野が狭い」⇒「立場が下」

に (ムリヤリな) 三段論法を適用しているだけなのではないのだろうか。 自身の視野の狭さを棚に上げておいて、 「プログラマだから立場が低い」と嘆くのはいかがなものかと思う。 もちろん、 「プログラミングに特別な能力が必要」なのではなくて、 「優れたプログラムを書くには特別な能力が必要」が正しい。 「下っ端プログラマ」と「優秀なプログラマ」を同列に扱ってはいけない。

前述した大森氏の注釈は、次のように続く:

この問題に関しては「Life is beautiful」というブログの 「ソフトウェアの仕様書は料理のレシピに似ている」というエントリが参考になる。

さっそくこの中島聡氏のブログを 読んでみた。

ソフトウェアのアーキテクトが自らプログラムを書いたり、 下っ端のエンジニアの書いたコードをレビューするのは、 レストランのシェフが自ら料理をしたり、 下っ端の料理人の作ったスープの味見をするとの同じである。

とても適切な喩えのように思える。 しかしこの伝で行けば、

下っ端のプログラマ(料理人)がアーキテクト(シェフ)になれるかは 才能に依存するところが多分にあり、 適性がない人は、いくら努力しても永久に下っ端のまま、 一人月 30万円レベルから抜け出せない。 そういう人はさっさとプログラミング(料理)をあきらめて、 マネジメントをやったほうがいい。 マネジメントなら才能が無くても経験次第でそれなりのレベルにはなる。

というとても残酷ではあるが、 とても実状に近い類推が出てきてしまうのだが...

ちなみに中島氏のブログには

優秀なエンジニアとそうでないエンジニアの生産性は(誇張抜きで) 20対1ぐらいである。

という話が出てくるのだが、私はもっと差があると感じている。 超優秀なプログラマと、下っ端プログラマとでは、 生産性が(誇張抜きで) 3桁違う、 というのが私の主張。 トッププログラマの 1000分の1の生産性しかない、 そもそもプログラミングに適性がない人が 無理矢理プログラマを目指そうとするから、不幸が始まる。 しかも、プログラミングは適性のない人でも時間さえかければ (かろうじて)動くプログラムが作れてしまったりするから余計タチが悪い。

これが料理であれば適性のない人は何日かけたって美味しい料理は作れないわけで、 早い段階で才能の無さを自覚して他の職を探すものだと思うが、 下っ端プログラマにはその自覚がない。 自覚が無いからプログラマへの道をあきらめめようとしない。 仕方ないので 35歳くらいで周囲が引導を渡すわけで、 これが「プログラマ35歳定年説」の正体。

- o -

ここまでの話と全然関係ないが、大森氏の記事に

BASICというプログラミング言語の文法を知ったのは確か中学生のときだし, 高校生のときにはシャープの「MZ-80K2E」というマイコンで 簡単なゲームのプログラムくらいは書いていた。

と書いてあったのが、 実はこの記事を引用しようと思った真の理由だったりする (^^;)。 私も中学一年の終わり頃 BASIC を知り、 高校生のとき MZ-80K2E でゲームを作ったりして遊んでいた。

Filed under: 技術者の成長 — hiroaki_sengoku @ 06:52
2006年4月4日

人の上に立とう (1)

人の上に立とう、といっても、威張りちらしたい、というわけではない。 相手より高い視点を持てるよう努力しよう、ということだ。

大局的な判断ができる人がリーダシップをとり、 局所的な判断しかできない人は、その指示に従うことになる。 上司が指示を出し、部下がそれに従う、というわけではない。 もちろん指揮命令系統が決められている場合は、 そこから逸脱するのは難しいかも知れないが、 職種が違う場合はどうだろう?

セールスがお客様の言葉を錦の御旗よろしく振りかざして マーケタへマーケティング戦略を指示し、 マーケタが製品のあるべき姿をプランナへ指示し、 プランナが製品仕様を決めて技術者に指示する。

こうした指示の連鎖は、よく見かける光景だが、 セールス、マーケタ、プランナ、技術者は、 本来は上下関係はないはずである。 なぜこのような指示する側とされる側に分かれてしまうのか。

で、上下関係をひっくり返してみようと思った。 技術者である私が、セールスやマーケタやプランナに指示を出してみる。

もちろん、いきなり指示を出そうと思っても、 何を指示すべきか分からないから、準備が必要である。

まず営業同行して、セールスの人達がお客様と何を話しているか観察。 ところがお客様の顔がいかにも興味なさそうなのに、 とうとうと自社製品の説明を立て板に水のごとく話している姿を目撃。 う~ん、素人の私にだって、こういう営業方法が 論外なことくらいは分かる。 もちろん優秀な営業マンもいるのだが、 私の目から見てもダメダメな人がいるというのは発見だった。

つぎにマーケ本部の定例ミーティングに参加してみる。 最初のうちは全貌が把握できていないのでおとなしくしているが、 すぐに我慢ができなくなった。 自社のポジションがまるで理解できていない。 弊社は大企業とは違うのだ。 弊社のロゴを見たって誰も弊社のことを思い浮かべたりはしない。 それにいま攻めようとしている市場は導入期である。 新しいカテゴリを作ろうとしているのだから 成熟期の商品と同じ攻め方でいいわけがない。 もちろん優秀なマーケタもいるのだが、 マーケティングの教科書を一読だけで分かるようなことを押さえずに、 見よう見まねの SWOT 分析などをして マーケタ気取りの人がいるというのは発見だった。

プランナは、技術者に近い。技術者と同じく「作る」職種である。 仕様を考えつつどんどんプロトタイピングして実ユーザの声を聞く、 という開発手法もあるわけで、 プランナと技術者は対等の関係になることも多い。

セールスやマーケタは、「売る」職種である。 技術者とは別人種と言ってもいいだろう。 だからといって「売る」職種が、「作る」職種よりも 立場が上というわけではない。 単に「売る」職種の方が広い視野を持ちやすく、 「作る」職種が、ともすると目先の問題に固執しがち というだけのこと。

Filed under: 技術者の成長 — hiroaki_sengoku @ 08:38
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月3日

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

KLab が運用しているコンテンツで使用している DB サーバは、
パスワードを知っているだけではアクセスできません。
どういう仕掛けになっているかの説明は後のお楽しみ、ということで
まずは前振りから...

パスワードというのは知る人が多くなればなるほど漏れるものですし、
ひとたび漏れてしまえば、どこまで漏れるかコントロール不能です。
したがって DB に限らず、KLab ではパスワードへの依存を
できる限り減らしています。

例えば、サーバへのログインは、必ず ssh の RSA 認証を使うことを必須とし、
コンテンツ管理のためのページでは SSL クライアント認証を行なっています。
つまり、秘密鍵を持っている人のみがアクセスできるようにしておいて、
秘密鍵自体は各人の責任において管理してもらう、という方式です。
万一、サーバへ不正なアクセスが行なわれたら、
誰の秘密鍵によってアクセスされたかログが残りますから、
誰の責任か明確になるわけです。

ところが、プログラムが DB サーバなどへアクセスする場合は事情が
変わってきます。まず、

(1) プログラムには責任を負わせることができない ;-)

そのプログラムを開発したり運用したりする人が、
秘密鍵を管理するしかありませんが、
秘密鍵というのはあくまで一人の個人が管理するからこそ
責任を負わせられるのであって、
共同管理ということにしてしまっては、
万一漏れた時に、どこから漏れたのか調べようがなくなります。

かといって、そのプログラムが動作する時に用いる鍵の管理を、
一人の個人が管理するルールにしてしまうと、
運用がとても大変になります。
障害が起きた時に、その人がいなければ原因究明もままなりません。

二番目の問題として、

(2) DB アクセスには高いパフォーマンスが要求される

人がサーバ等へログインするときは、所詮は人間のスピードですから、
大したレスポンススピードは必要ありません。
秘密鍵による認証を行なう余裕は充分にあります。
一方、Web アプリケーション等のプログラムが DB サーバへアクセスする場合は、
ページのヒット数が高ければ高いほど、高いパフォーマンスが要求されます。

コネクションをプールするなどの手法によって、
認証コストを下げることは可能であるものの、
プログラム-DBサーバ間の通信路の暗号化などを行なっていては、
暗号化処理のぶんだけサーバ負荷が余計にかかります。
つまり DB サーバへのアクセスに SSL クライアント認証を行なう、
などの方法は、なるべくなら避けたいところです。

ではどうすればいいでしょうか?
(続きは次回に)

Filed under: システム構築・運用 — hiroaki_sengoku @ 11:20
2006年4月3日

IPv6 (2) hatena_b

IPv6ブリッジ機能付きルーター (ブルータ) を使えば、 NAT 内の LAN でそのまま IPv6 を使えるわけで、 IPv6 の敷居は一気に下がる。

つまり、ほとんどの OS がすでに IPv6 をサポートしているので、 ルータが IPv6 を素通し (ブリッジ) しさえすれば、 LAN 内で IPv6 を意識せずに使える。

今まで通り IP を (NAT 経由したプライベートアドレスで) 使いながら、IPv6 のオイシイところ (end-to-end 通信ができる) だけを ツマミ食いできるわけで、 今度こそ (^^;) IPv6 が普及するのではないかと...

Linux でブルータを実現するには、 まずブリッジ機能をカーネルに組み込んでおく。

# brctl show br0
bridge name     bridge id               STP enabled     interfaces
br0             8000.00022Axxxxxx       no              eth0
                                                        eth1

このままだと IP までブリッジしてしまうので、 IP に対してはルータとして機能し、 IPv6 に対してだけブリッジさせるために、ebtables を使う。

# ebtables -t broute -L
Bridge table: broute

Bridge chain: BROUTING, entries: 3, policy: DROP      …… (3)
-j mark --set-mark 0x0 --mark-target CONTINUE         …… (0)
-p IPv6 -i eth0 -j mark --set-mark 0x600              …… (1)
-p IPv6 -i eth1 -j mark --set-mark 0x601              …… (2)

上記設定の意味は、

(0) まず全パケットに対し 0x0 のマークをつける。
    で、
(1) eth0 から入ってきた (LAN内から外へ出ていく)
    IPv6 パケットは、0x600 のマークをつけてブリッジする。
(2) 逆に eth1 から入ってきた (外から入ってきた)
    IPv6 パケットは、0x601 のマークをつけてブリッジする。
(3) それ以外 (IP パケットや PPPoE パケット) はブリッジしない (DROP)
    つまり IP に対してはルータとして機能する

さらに ip6tables を使ってフィルタリングする。

ip6tables -P FORWARD DROP                                       …… (*)
ip6tables -I FORWARD -j incoming -m mark --mark 0x601/0xffff    …… (2)
ip6tables -I FORWARD -j ACCEPT -m mark --mark 0x600/0xffff      …… (1)
(1) 0x600 のマークがついているパケットは、素通し (ACCEPT) する。
(2) 0x601 のマークがついているパケットは、incoming チェインへ渡す。

incoming チェインで ACCEPT されなかったパケットは、
(*) FORWARD チェインのポリシーに従って、DROP される。

ebtables でマーク付して ip6tables でそのマークに基づいて フィルタリングするという、 まわりくどい方法をとっているのは、 ebtables はあくまで L2 のフィルタなので IPv6 アドレスに基づいたフィルタリングができず、 逆に ip6tables は L3 のフィルタなので eth0/eth1 はまとめてブリッジインタフェース br0 としてしか見えず、 どちらの物理インタフェースから入ってきたパケットか 判別できないため。

ちなみに iptables だと、 L3 フィルタといいながら physdev モジュールがあるので、 ブリッジのどちら側から入ってきたか判別できたりする。 ip6tables の機能拡張に期待したいところ。

Filed under: IPv6,システム構築・運用 — hiroaki_sengoku @ 07:20
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
« Newer PostsOlder Posts »