ntpd packet logのための覚え書き

これは何?

  • ntpdが送受信するパケットを記録したいと仮定する。

  • ntpdのログ機構では、どうやらそこまで細かいログを取ることが出来ない。

  • そこでちょっとソースを見てみる。

  • 覚え書きなのできっと勘違いや誤りがあるに違いないです。先にごめんなさいしておきます。

  • 追記、受信側だけで長くなったので送信側は稿を改めます。

NTPd

  • まず、ntpd本家githubリポジトリはこちら。

  • FreeBSD 12.1-RELEASEでは、/usr/src/usr.sbin/ntp(実態はほぼ/usr/src/contrib/ntp側にある)としてntp.orgのntpdを同梱している。

  • NTPの他の実装としては次のふたつが有名処か。

    • OpenNTPD

    • Chrony

      ntpdがパケットを受信する流れ

      • ntpd/ntpd.cmain()はこの辺。

      • main()ではコマンドライン引数をparseした後、ntpdmain()を呼び出す

        #ifdef NO_MAIN_ALLOWED
        CALL(ntpd,"ntpd",ntpdmain);
        #else/* !NO_MAIN_ALLOWED follows */
        #ifndef SYS_WINNT
        int
        main(
        int argc,
        char *argv[]
        )
        {
        return ntpdmain(argc, argv);
        }
        #endif /* !SYS_WINNT */
        #endif /* !NO_MAIN_ALLOWED */
      • ntpdmain()ではいろいろと初期設定をした後に他にやることがなくなると(?)、io_handler()を呼び出す

        if (!was_alarmed && !has_full_recv_buffer()) {
        /*
         * Nothing to do. Wait for something.
         */
        io_handler();
        }
      • io_handler()ntpd/ntp_io.cにあってこの辺から始まり、input_handler_scan()を呼び出す

        if (nfound > 0) {
        l_fp ts;
        ​
        get_systime(&ts);
        ​
        input_handler_scan(&ts, &rdfdes);
        } else if (nfound == -1 && errno != EINTR) {
        msyslog(LOG_ERR, "select() error: %m");
        }
        # 
      • シグナルハンドラから呼ばれるinput_handler()もあるが、結局input_handler_scan()を呼んでいる。

          }
           if (n > 0)
            input_handler_scan(cts, &fds);
        }
      • input_handler_scan()では、まずread_refclock_packet()を呼び出してReference Clockをチェックする。これがrefclockすなわち精密な時刻情報を提供する装置(通常ローカルにある)とのやり取りを担当するのであろう。

        #ifdef REFCLOCK
        /*
         * Check out the reference clocks first, if any
         */
        
        for (rp = refio; rp != NULL; rp = rp->next) {
        fd = rp->fd;
        
        if (!FD_ISSET(fd, pfds))
        continue;
        buflen = read_refclock_packet(fd, rp, ts);
      • 次いでinput_handler_scan()read_network_packet()を呼び出してNetwork packetをチェックする。こちらはおそらくNTPサーバとの間やその他の通信を担当するのであろう。

        if (FD_ISSET(fd, pfds))
        do {
        buflen = read_network_packet(
        fd, ep, ts);
        } while (buflen > 0);
      • read_network_packet()では、struct recvbuf *rbにメモリ領域を確保してrecvfromないしrecvmsgからのデータを記録する。

        #ifndef HAVE_PACKET_TIMESTAMP
        rb->recv_length = recvfrom(fd, (char *)&rb->recv_space,
           sizeof(rb->recv_space), 0,
           &rb->recv_srcadr.sa, &fromlen);
        #else
        iovec.iov_base        = &rb->recv_space;
        iovec.iov_len         = sizeof(rb->recv_space);
        msghdr.msg_name       = &rb->recv_srcadr;
        msghdr.msg_namelen    = fromlen;
        msghdr.msg_iov        = &iovec;
        msghdr.msg_iovlen     = 1;
        msghdr.msg_control    = (void *)&control;
        msghdr.msg_controllen = sizeof(control);
        msghdr.msg_flags      = 0;
        rb->recv_length       = recvmsg(fd, &msghdr, 0);
        #endif
      • さらにadd_full_recv_buffer()を呼び出して受信したデータを受信バッファに追加する。この時、rb->receiverreceiveを設定している点に注意。この要素はコールバック関数で、receiventpd/ntp_proto.cで定義された関数である(後述)。

        rb->recv_time = ts;
        rb->receiver = receive;
        ​
        add_full_recv_buffer(rb);
        ​
        itf->received++;
        packets_received++;
        return (buflen);
        }
      • add_full_recv_buffer()libntp/recvbuff.cにあって、渡されたバッファrbfull_recv_fifo変数に追加する。

        void
        add_full_recv_buffer(recvbuf_t *rb)
        {
        if (rb == NULL) {
        msyslog(LOG_ERR, "add_full_recv_buffer received NULL buffer");
        return;
        }
        LOCK();
        LINK_FIFO(full_recv_fifo, rb, link);
        full_recvbufs++;
        UNLOCK();
        }
      • recvbuf_t型ことstruct recvbuf型はinclude/recvbuff.hで定義されている。要素にコールバック関数void (*receiver)(struct recvbuf *)がある点に注意。

        typedef struct recvbuf recvbuf_t;
        ​
        struct recvbuf {
        recvbuf_t *link;/* next in list */
        union {
        sockaddr_uX_recv_srcadr;
        caddr_tX_recv_srcclock;
        struct peer *X_recv_peer;
        } X_from_where;
        #define recv_srcadrX_from_where.X_recv_srcadr
        #definerecv_srcclockX_from_where.X_recv_srcclock
        #define recv_peerX_from_where.X_recv_peer
        #ifndef HAVE_IO_COMPLETION_PORT
        sockaddr_usrcadr;/* where packet came from */
        #else
        intrecv_srcadr_len;/* filled in on completion */
        #endif
        endpt *dstadr;/* address pkt arrived on */
        SOCKETfd;/* fd on which it was received */
        intmsg_flags;/* Flags received about the packet */
        l_fprecv_time;/* time of arrival */
        void(*receiver)(struct recvbuf *); /* callback */
        intrecv_length;/* number of octets received */
        union {
        struct pktX_recv_pkt;
        u_charX_recv_buffer[RX_BUFF_SIZE];
        } recv_space;
        #definerecv_pktrecv_space.X_recv_pkt
        #definerecv_bufferrecv_space.X_recv_buffer
        int used;/* reference count */
        };
      • get_full_recv_buffer()full_recv_fifo変数から受信バッファをひとつ取り出して返り値として返す。

        recvbuf_t *
        get_full_recv_buffer(void)
        {
        recvbuf_t *rbuf;
        ​
        LOCK();
        
        #ifdef HAVE_SIGNALED_IO
        /*
         * make sure there are free buffers when we
         * wander off to do lengthy packet processing with
         * any buffer we grab from the full list.
         * 
         * fixes malloc() interrupted by SIGIO risk
         * (Bug 889)
         */
        if (NULL == free_recv_list || buffer_shortfall > 0) {
        /*
         * try to get us some more buffers
         */
        create_buffers(RECV_INC);
        }
        #endif
        ​
        /*
         * try to grab a full buffer
         */
        UNLINK_FIFO(rbuf, full_recv_fifo, link);
        if (rbuf != NULL)
        full_recvbufs--;
        UNLOCK();
        ​
        return rbuf;
        }
      • このget_full_recv_buffer()を呼び出すのはntpdmain()の中のこの辺である。rbuf->receiverNULLでない時に(*rbuf->receiver)(rbuf);でコールバック関数を呼び出している点に注意。

        rbuf = get_full_recv_buffer();
        while (rbuf != NULL) {
        if (alarm_flag) {
        was_alarmed = TRUE;
        alarm_flag = FALSE;
        }
        UNBLOCK_IO_AND_ALARM();
        ​
        if (was_alarmed) {
        /* avoid timer starvation during lengthy I/O handling */
        timer();
        was_alarmed = FALSE;
        }
        ​
        /*
         * Call the data procedure to handle each received
         * packet.
         */
        if (rbuf->receiver != NULL) {
        # ifdef DEBUG_TIMING
        l_fp dts = pts;
        ​
        L_SUB(&dts, &rbuf->recv_time);
        DPRINTF(2, ("processing timestamp delta %s (with prec. fuzz)\n", lfptoa(&dts, 9)));
        collect_timing(rbuf, "buffer processing delay", 1, &dts);
        bufcount++;
        # endif
        (*rbuf->receiver)(rbuf);
        } else {
        msyslog(LOG_ERR, "fatal: receive buffer callback NULL");
        abort();
        }
        ​
        BLOCK_IO_AND_ALARM();
        freerecvbuf(rbuf);
        rbuf = get_full_recv_buffer();
        }
      • receive()でパケットのフィールドを分別し始めるのはこの辺りから。pktに受信したパケットのメモリアドレスを保持して、hisversion, hisleap, hismode, hisstratumを読み出している。余談だが、これもhisはダメで三人称単数のtheirにしろとかいうよくわからない話になるのであろうか。

         register struct pkt *pkt;
              :
         pkt = &rbufp->recv_pkt;
         DPRINTF(2, ("receive: at %ld %s<-%s flags %x restrict %03x org %#010x.%08x xmt %#010x.%08x\n",
            current_time, stoa(&rbufp->dstadr->sin),
               stoa(&rbufp->recv_srcadr), rbufp->dstadr->flags,
            restrict_mask, ntohl(pkt->org.l_ui), ntohl(pkt->org.l_uf),
               ntohl(pkt->xmt.l_ui), ntohl(pkt->xmt.l_uf)));
        hisversion = PKT_VERSION(pkt->li_vn_mode);
        hisleap = PKT_LEAP(pkt->li_vn_mode);
        hismode = (int)PKT_MODE(pkt->li_vn_mode);
        hisstratum = PKT_TO_STRATUM(pkt->stratum);
      • ところで、このstruct pktinclude/ntp.hこの辺りで定義されている。上で使われているli_vn_modestratumのような要素があることがわかる。

        /*
        * NTP packet format. The mac field is optional. It isn't really
        * an l_fp either, but for now declaring it that way is convenient.
        * See Appendix A in the specification.
        *
        * Note that all u_fp and l_fp values arrive in network byte order
        * and must be converted (except the mac, which isn't, really).
        */
        struct pkt {
        u_charli_vn_mode;/* peer leap indicator */
        u_charstratum;/* peer stratum */
        u_charppoll;/* peer poll interval */
        s_charprecision;/* peer clock precision */
        u_fprootdelay;/* roundtrip delay to primary source */
        u_fprootdisp;/* dispersion to primary source*/
        u_int32refid;/* reference id */
        l_fpreftime;/* last update time */
        l_fporg;/* originate time stamp */
        l_fprec;/* receive time stamp */
        l_fpxmt;/* transmit time stamp */
        ​
        #defineLEN_PKT_NOMAC(12 * sizeof(u_int32)) /* min header length */
        #define MIN_MAC_LEN(1 * sizeof(u_int32))/* crypto_NAK */
        #define MAX_MD5_LEN(5 * sizeof(u_int32))/* MD5 */
        #defineMAX_MAC_LEN(6 * sizeof(u_int32))/* SHA */
        ​
        /*
         * The length of the packet less MAC must be a multiple of 64
         * with an RSA modulus and Diffie-Hellman prime of 256 octets
         * and maximum host name of 128 octets, the maximum autokey
         * command is 152 octets and maximum autokey response is 460
         * octets. A packet can contain no more than one command and one
         * response, so the maximum total extension field length is 864
         * octets. But, to handle humungus certificates, the bank must
         * be broke.
         *
         * The different definitions of the 'exten' field are here for
         * the benefit of applications that want to send a packet from
         * an auto variable in the stack - not using the AUTOKEY version
         * saves 2KB of stack space. The receive buffer should ALWAYS be
         * big enough to hold a full extended packet if the extension
         * fields have to be parsed or skipped.
         */
        #ifdef AUTOKEY
        u_int32exten[(NTP_MAXEXTEN + MAX_MAC_LEN) / sizeof(u_int32)];
        #else/* !AUTOKEY follows */
        u_int32exten[(MAX_MAC_LEN) / sizeof(u_int32)];
        #endif/* !AUTOKEY */
        };
      • さて、receive()に戻って、hisversionからhisstratumまでを取り出した後、restrict_maskRES_IGNOREだったら (正確にはそのビットが立っていたら)単純に無視する。このrestrict_maskは少し戻ったところで受信パケットのソースアドレスから計算されている

        restrict_mask = restrictions(&rbufp->recv_srcadr);
        pkt = &rbufp->recv_pkt;
        DPRINTF(2, ("receive: at %ld %s<-%s flags %x restrict %03x org %#010x.%08x xmt %#010x.%08x\n",
            current_time, stoa(&rbufp->dstadr->sin),
            stoa(&rbufp->recv_srcadr), rbufp->dstadr->flags,
            restrict_mask, ntohl(pkt->org.l_ui), ntohl(pkt->org.l_uf),
            ntohl(pkt->xmt.l_ui), ntohl(pkt->xmt.l_uf)));
        hisversion = PKT_VERSION(pkt->li_vn_mode);
        hisleap = PKT_LEAP(pkt->li_vn_mode);
        hismode = (int)PKT_MODE(pkt->li_vn_mode);
        hisstratum = PKT_TO_STRATUM(pkt->stratum);
        if (restrict_mask & RES_IGNORE) {
        sys_restricted++;
        return;/* ignore everything */
        }
      • というわけで、ntpdが受け取るパケットのログを取るならこの辺りhisversionその他を取り出した直後が良いであろう。ここなら各種制約に掛かって捨てられるパケットも見える位置である。

      • 続いてhismode等を見ながらmode 7の処理をntpd/ntp_request.cprocess_private()に渡し、mode 6の処理をntpd/ntp_control.cprocess_control()に渡している。mode 6,7のパケットフォーマットを知るにはこれら関数(とその先)を見れば良いようだ。

      • (*rbuf->receiver)(rbuf);でコールバック関数を呼び出すということは、add_full_recv_buffer()でコールバックに設定したreceive()を呼ぶということである。この関数はntpd/ntp_proto.cで定義されたreceive()であると思われる。

      • 受信バッファrecvbufについてはlibntp/recvbuff.cに一通りの定義があるようだが、受信バッファをadd_full_recv_buffer()で追加し、get_full_recv_buffer()で取り出すという動作であるようだ。

      • input_handler_scan()はこの辺から。

どうしてそんな?

  • GCP GCEでFreeBSD12Rなノードを建てた。
    • GCPはGoogleのクラウド全般を指し、GCEはその中の仮想マシン貸し出しサービスを指す。
    • AmazonでいうところのAWSEC2だと思っている。
  • GCEではOP25Bが適用されていて、平たく言うと建てたノードからメールを出せない。システム管理上、daily checkのメールは非常に便利なので、これは困る。
  • 正しくはegressのSMTP(25/tcp)はブロックされているがSMTPS(465/tcp)とSubmission(587/tcp)は開いているのでそっちを使えば出せないわけではない。
  • ところが、FreeBSD同梱のsenmail(/etc/mailに設定ファイルがあるやつ)は、自分発のコネクションをSSL(SMTPS)やTLS(Submission)で包むことが出来ない。(みたい。できるなら是非教えて下さい。)
  • そこで、stunnelで自宅サーバのSMTPSポートにSSLトンネルを張っておいて、sendmailからSMART_HOSTでこのトンネルへ投げ込むことを考えた。
    • SMART_HOSTはsendmailの機能のひとつで、メール配送で自ノードの外へ送るメールを(通常ならMXやAレコードを参照しながら各相手先ノードのSMTPサーバへ送るところを)全部まとめてSMART_HOSTに指定されたノードへ送るもの。
  • 蛇足ながらSMTPSはSSLで包むことを別にすればSMTPと同じ動きで、SMTP-AUTHなしでもメール配送はできる(第三者リレーなんかは設定で止めてあるのが普通で、その場合、自ノードで終端するメールだけ受け取る)。SMTP-AUTHで認証すればSMART_HOSTから見て外部へのメールもリレーする(一般論として)。SubmissionはSMTP-AUTHでの認証なしでは何もできないので、上の構成で作るならSMTPSでなければならないことになる。

参考URL

 

 

  • はっきり言えば、上のふたつの記事を読めば全部書いてある。神のお告げの如き記事であります。感謝。
  • 前者はFreeBSD同梱のsendmailについてSMART_HOST設定のやり方を説明しており、FreeBSD特有の扱い(makeとか)も出ている。
  • 後者はsendmail一般(というかFedora)について説明していて、特にSMART_HOSTで使用ポートを変更する方法が出ている。

目指す構成

  • このノードのsendmail発のメールをすべてSMART_HOST=127.0.0.1:10025へ送る。
  • stunnelで自宅サーバのSMTPSポートから127.0.0.1:10025までトンネルを掘る。SSLで包む部分はstunnelがやってくれるので、sendmailは127.0.0.1:10025でSMTPを喋ればよい。

stunnel設定

  • portsからsecurity/stunnelをインストール。
  • /usr/local/etc/stunnelにあるstunnel.conf-sampleを参考にstunnel.confを作成する。
include = /usr/local/etc/stunnel/conf.d

 

[fd-smtp]

client = yes

accept = 127.0.0.1:10025

connect = example.org:465

verifyChain = no

  • includeの先にはpidファイルの設定が入っているので省略できない。(serviceコマンドもしくはrc scriptがpidを見失う)
  • /etc/rc.conf.localにstunnel_enable="YES"を書いて、service stunnel startすればトンネルが張れる。

stunnel_enable="YES"

# service stunnel start

  • 本当はSSLサーバ証明書の検証をやるべきだが、ちょっとまだ手が回っていない。
  • 試験としては、まずopensslでstunnel抜きでの接続を確認し、次いでstunnel経由での接続を確認する。どちらも220 example.org ESMTPのようなSMTPバナーが出てくればとりあえずつながっていることがわかる。
# openssl s_client -connect example.org:465
220 example.org ESMTP
EHLO...
# nc 127.0.0.1 10025
220 example.org ESMTP

FreeBSD特有の/etc/mailでの動き

  • FreeBSD同梱のsendmailは、初期状態では自ノード内部からの要求(mailコマンドなど)を受けて自ノード内のユーザメールボックスまたは外部のメールサーバへの転送を行う。外部からのメールを受信することはしない。
  • 設定ファイルなどは/etc/mailにあり、makeコマンドを活用して動作する。以下に概要を説明する。
  • 直接のsendmailの設定ファイルは/etc/mail下のsendmail.cfとsubmit.cfで、初期状態でも上記の動きをする。
  • 設定を変更する場合は、まずmakeすることで自ノードのホスト名を関した一連のファイルを生成する。自ノード名をmynodeとすれば次のような形。

# cd /etc/mail

# make

cp -f freebsd.mc mynode.mc

/usr/bin/m4 -D_CF_DIR_=/usr/share/sendmail/cf/   /usr/share/sendmail/cf/m4/cf.m4 mynode.mc > mynode.cf

cp -f freebsd.submit.mc mynode.submit.mc

/usr/bin/m4 -D_CF_DIR_=/usr/share/sendmail/cf/   /usr/share/sendmail/cf/m4/cf.m4 mynode.submit.mc > mynode.submit.cf

  • 独自の設定変更はmynode.mcおよびmynode.submit.mcで行い、makeすることでmynode.cfおよびmynode.submit.cfを生成する。
  • 生成されたcfファイル群で問題なければmake installすることで本番cfファイル群(sendmail.cf, submit.cf)にコピーする。

# make install

install -m 444 mynode.cf /etc/mail/sendmail.cf

install -m 444 mynode.submit.cf /etc/mail/submit.cf

  • make restartまたはservice sendmail onerestartすることで新しい設定でsendmailデーモンを稼働させる。

# make restart

Restarting: sendmail sendmail-clientmqueue.

sendmail設定の修正

  • mynode.mcへの修正

# diff -u mynode.mc.orig mynode.mc

--- mynode.mc.orig    2020-10-01 19:19:00.927969000 +0900

+++ mynode.mc    2020-10-01 19:26:52.062137000 +0900

@@ -87,6 +87,10 @@

 

 dnl Dialup users should uncomment and define this appropriately

 dnl define(`SMART_HOST', `your.isp.mail.server')

+define(`SMART_HOST', `smtp:[127.0.0.1]')dnl

+define(`RELAY_MAILER_ARGS', `TCP $h 10025')dnl

+define(`ESMTP_MAILER_ARGS', `TCP $h 10025')dnl

+dnl FEATURE(`authinfo', `hash /etc/mail/authinfo.db')dnl

 

 dnl Uncomment the first line to change the location of the default

 dnl /etc/mail/local-host-names and comment out the second line.

  • mynode.submit.mcへの修正

# diff -u mynode.submit.mc.orig mynode.submit.mc

--- mynode.submit.mc.orig    2020-10-01 19:19:00.949538000 +0900

+++ mynode.submit.mc    2020-10-01 19:28:44.543670000 +0900

@@ -22,5 +22,8 @@

 define(`confDONT_INIT_GROUPS', `True')dnl

 define(`confBIND_OPTS', `WorkAroundBrokenAAAA')dnl

 dnl

+define(`SMART_HOST', `smtp:[127.0.0.1]')dnl

+define(`RELAY_MAILER_ARGS', `TCP $h 10025')dnl

+define(`ESMTP_MAILER_ARGS', `TCP $h 10025')dnl

 dnl If you use IPv6 only, change [127.0.0.1] to [IPv6:::1]

 FEATURE(`msp', `[127.0.0.1]')dnl

  • ふたつのmcファイルを修正する必要がある点に注意。
  • SMART_HOSTの設定では、smtpプロトコルで[127.0.0.1]へすべてのメールをリレーする設定。IPアドレスが[ ]に入っているのでMXを牽いたりせずに直接IPアドレスに接続せよという設定。
  • そのままでは25/tcpへ接続にいくので、次の二行で10025番ポートを指定。
  • 設定を反映して動作させるために
# cd /etc/mail
# make install restart
  • SMTP-AUTHでログインしたければFEATUREのauthinfoを設定する。上記参考URL参照。ただ、第三者リレーを許可することになるので、スパムをばらまかないように注意すること。ここでは設定しないのでSMART_HOSTが受け取らないメールはフェイルする。

第三者転送と見られないために

  • ここまでの設定でmynodeで発したメールはすべてSMART_HOSTへ送られる状態。
  • なので、SMART_HOST側で受け取るドメイン名に宛てたメールは受け取ってもらえる。
  • しかし、mynodeから出ていくメールには、Envelope Fromにmynodeのドメイン名が付くものがあって、これは届かない。例えばこうやって出したメール。
mynode$ date | mail -s test someone
  • おまけに、GCEにインストールしたFreeBSD12Rでは、/etc/hostsに自動的にエントリを追加する仕組みがある(使用したイメージの設定によると思われる)ので、こんなエントリが見えることになる。しかもどうやら延々とこの2行がつかされ続けるバグがあるようだ。

(/etc/hosts)

 

10.xxx.xxx.2 mynode.europe-north1-a.c.s${project_name}.internal mynode  # Added by Google

169.254.169.254 metadata.google.internal  # Added by Google

  • 対策は、この2行が出現するより前の行に自ノードの名前を書いておくこと。

(/etc/hosts)

 

10.xxx.xxx.2 mynode.my.domain mynode

 

10.xxx.xxx.2 mynode.europe-north1-a.c.s${project_name}.internal mynode  # Added by Google

169.254.169.254 metadata.google.internal  # Added by Google

(この後繰り返しこの2行が追加される。助けて)

  • こうすることでmailコマンドで次ノードのユーザ名宛に送ったメールでもEnvelope Fromに@mynode.my.domainが補完されて出ていくことになる。
 
  • これでもなおSMART_HOST側では受け取らない。なぜなら、SMART_HOST側のMTAが@mynode.my.domainを自分が受け取るべきメールだと認識していないから。
  • SMART_HOST側はPostfixなので、main.cfのmydestinationに追記すれば良い。

感想

  • 21世紀も1/5が過ぎたというのにsendmail設定にハマるとは思わなかったが、それはそれで楽しかった。
  • stunnelの近端側に25/tcpポートを使えるなら、ひょっとしたらmailertableでルーティングするだけで済むかもしれない。この場合は宛先アドレスのドメイン名ごとに設定を分けることもできるだろう。ifconfig lo0 alias 127.0.0.2/32とかやればできないわけではない。
  • PostfixやOpenSMTPdでやるとどうなるかしら。
  • sendmailのドキュメントがオンラインでは見当たらない。https://sendmail.org/もどこかへリダイレクトされるし。
  • mcファイルの書き方は/etc/mail/mynode.mc冒頭の記述によれば/usr/share/sendmail/cf/READMEか/usr/src/contrib/sendmail/cf/READMEを見よとのこと。
  • 今回の構成では、mynode発第三者宛のメールはフェイルする。これを救うにはSMTP-AUTHで認証すれば良いと思うが、その場合にはspamも中継してしまうので要注意。

FreeBSD12.1RをGCP/GCEでインストール

これは何?

  • 例によって自分のための覚え書き。
  • 随分前からGoogle Cloud Platform (GCP)の仮想マシン(VM)提供サービスであるGoogle Compute Engine(GCE)でFreeBSDが使える。メディアを準備してくださっているFreeBSDの(Googleの?)中の人に感謝。
  • 一度やってみようということで、インストールした。

gcloud SDK

  • WebUIからインストールに行くとLinux系のメディアはあれどFreeBSDは見当たらないので、コマンドラインからやる。
  • ほとんどこちらのマイナビニュースから勉強させていただいたので、正しくはこっちを見てください。
  • 僕の場合は手元はMacOSだがWindows版もあるみたい。
  • いずれにしてもGoogle Cloud SDKなるPythonパッケージをインストール・初期化すること。
  • GCPアカウントの設定もしておくとよいが、必要な時にSDKから聞かれるみたいなのでその時でもいいかも。

インスタンス作成

  • FreeBSDのインストールメディア、GCE的にはイメージというらしいが、そのリストを得るにはマイナビさんの教えに従って出来た。
$ gcloud compute images list --project freebsd-org-cloud-dev --no-standard-images

NAME                                     PROJECT                FAMILY             DEPRECATED  STATUS

    (中略)

freebsd-12-1-release-amd64               freebsd-org-cloud-dev  freebsd-12-1                   READY

    (後略)

  • インスタンス作成もマイナビさんの教えのとおりなんだけど、こんな感じ。

$ gcloud compute instances create myinstance1

  --zone europe-north1-a

  --machine-type e2-micro

  --network default

  --maintenance-policy MIGRATE

  --image freebsd-12-1-release-amd64

  --image-project freebsd-org-cloud-dev

  --boot-disk-size 22

  --boot-disk-type pd-ssd

  • zoneと言っているのはデータセンターぐらいのイメージで、マニュアルはここ。一応、コマンドで一覧を出力できるが...

 

$ gcloud compute zones list

NAME                       REGION                   STATUS  NEXT_MAINTENANCE  TURNDOWN_DATE

us-east1-b                us-east1                 UP

us-east1-c                 us-east1                 UP

us-east1-d                 us-east1                 UP

us-east4-c                 us-east4                 UP

us-east4-b                 us-east4                 UP

    (後略)

  • これだと「米国の東側」くらいしかわからないが、[consoleのCompute EngineからVM Instancesを選んでインスタンスを作り(Create Instance)に行く](https://console.cloud.google.com/compute/instancesAdd)と、Regionのプルダウンからもう少しわかる。コマンドラインで指定する時は、さらに-aとか-bが必要であるようだ。
Zone Location
northamerica-northeast1 Montreal
southamerica-east1 San Paulo
us-central1 Iowa
us-east1 South Carolina
us-east4 Northen Virginia
us-west1 Oregon
us-west2 Los Angeles
us-west3 Salt Lake City
us-west4 Las Vegas
europe-north1 Finland
europe-west1 Belgium
europe-west2 London
europe-west3 Frankfurt
europe-west6 Zurich
asia-east1 Taiwan
asia-east2 Hong Kong
asia-northeast1 Tokyo
asia-northeast2 Osaka
asia-northeast3 Seoul
asia-south1 Mumbai
asia-southeast1 Singapore
asia-southeast2 Jakarta
australia-southeast1 Sydney
 
  • machine-typeはかなり色々あって、ちょっとまだ良くわからない。とりあえずマニュアルはこちら。

     

  • ディスクサイズはもっと小さく10(GB)くらいにしたいのだが、イメージと同サイズ以上出ないと駄目と言ってエラーが出るので22に。
  • あとはマイナビ先生に従ってイメージその他を指定してコマンド実行。
  • インストールにはgcloud compute instances createコマンドが返ってきてからでも数分くらいかかるので、ちょっと休憩でもしてから次へ。

IPアドレスの永続化

  • 永続化であってるかどうかかなり怪しいが、上記で作成したインスタンスにはグローバルなIPv4アドレス1個が紐付いていて、このままでも下記のSSHアクセスは可能。これが「エフェメラル」とか「一時的」とか呼ばれるアドレスで、(多分)インスタンスの再起動で揮発する。
  • そこで「永続的」に使えるアドレスにしたいわけだが、それには

    の「エフェメラル外部 IP アドレスの昇格」の項目に従って手続きをすれば良い。

  • 「VPCネットワーク」から「外部IPアドレス」へ進んで、該当する行の「種類」を「静的」に変更するだけ。

インスタンスへsshアクセス

  • マイナビ先生によればインスタンスにsshでアクセスするには次のコマンドを。僕の場合はこのコマンドの途中でGCP的アカウントの指定やSSH鍵の作成などを促された。

gcloud compute ssh myinstance1 --zone europe-north1-a

  • defaultのゾーンで無いところにインスタンスを作ったせいか、ゾーン指定が必須であった。
  • SSH鍵は~/.ssh/google_compute_engine[.pub]にできており、~/.ssh/google_compute_known_hostsもあった。
  • configをいじった形跡はないので、それはgcloudコマンドが別の場所に持っているのであろう。

ファイアウォール設定

  • (2020/Sep/29追記)
  • defaultのファイアウォール定義は下図の4個で、Apply to allというのだから全部のVMに適用なのかしら。すべてAllowで対象がICMPだったりTCP/UDPの全ポート+ICMPだったり3389/tcpなRDPだったり22/tcpなSSHだったり。ちょっとこれではアレかもしれない。
  • WebUIのVPC Network/Firewallで定義できる。
  • ファイアウォール定義の中のTarget tagsに何か目印になるタグを書いておくこと。ここで書いたタグをVMに付与することで、そのファイアウォール定義がVMに適用されるということになる。らしい。
  • VMにタグを付与するのは、Compute Engine/VM instancesから個別のVMの設定の中のNetwork tagsで指定する。らしい。
  • なお、ルールの適用順序は定義の際のPriorityの昇順。らしい。

google-sudoers

  • (2020/Sep/29追記)
  • 上の手順でインストールするとsudoがすでにインストールされた状態になるのだが、google-sudoersなるグループを作ってあって、そのメンバーならパスワードなしで任意コマンドについてsudoできる。これは/etc/groupと/usr/local/etc/sudoers.d/google_sudoersに設定されている。
  • さすがにちょっと気持ち悪いのでNOPASSだけは外そうかな。

まとめ

  • Amazon Web Service EC2にはある程度なれていたので、似たようなものかと甘く見て手を出したが、GCP GCEはスパルタンであった。
  • もっとも、Linux系かWindows系のOSならWebUIからチョチョイのちょいなのかもしれない。

これは何?

  • AWS EC2にFreeBSD 12-RELEASEなノードを建てた時の備忘録
  • 多分自分の役にしか立たない。申し訳ない。

インスタンス作成

  • EC2のインスタンスを作成する。

ステップ1: マシンイメージ選択

  • マシンイメージは"AWS Marketplace"でFreeBSDを検索すると色々出てくる。(逆に言うと「クイックスタート」には存在しない)
  • Collin Percival先生がいつも良い仕事をしてくれています。感謝!
  • 最新のFreeBSD 12を選択。
  • EC2インスタンスの利用料金はかかるが、マシンイメージの利用料は無料。

ステップ2: インスタンスタイプの選択

  • 今回はt2.microで十分なのでこれ。
  • 永続的に使う気なので「終了保護の有効化(誤った終了を防止します)」だけはクリックした状態にしておく。
  • ストレージはデフォルトの10GBにする。/usr/srcや/usr/portsを展開する予定があるなら30GBくらい必要だが、今回はしないで良いはず。
  • あとは適宜サブネット割当とかアクセスポリシー割当とか。
  • SSHの鍵を作ったら死守する。このタイミングを逃すと二度と鍵を取り出す機会はない。

固定のIPv4アドレス割当

  • Elastic IPを割り当てないと再起動毎に別の(global) IPv4アドレスで上がってくるのでサーバとしてはちょっと使いにくい。
  • FQDNもamazonaws.comの下の長いものになるので、必要なら別に命名しておく。

初回アクセスでの設定変更

sshログインの方法

  • SSH公開鍵認証でログインしたらユーザとしてはec2-userで、suすればパスワード無しでrootになれる。すかさずec2-userとrootにパスワードを設定すべし。
  • ~/.ssh/configも書いておくほうが幸せ。

$ ssh -p 22 -l ec2-user -i <path to ssh key> <instance ip address>

# passwd root

# passwd ec2-user

  • ついでにec2-userのユーザ名(とホームディレクトリ名)を変更しておくと、さらに安心かもしれない。それには

# vipw --> :%s/ec2-user/<new id>/gc (2箇所)

# vi /etc/group --> :%s/ec2-user/<new id>/gc (2箇所)

# mv /home/ec2-user /home/<new id>

  • /etc/groupでwheelグループに加入しておかないとsuが使えない。

ホスト名

  • /etc/rc.conf.localで
hostname="new_host_name.example.com"
  • rebootで発効。すぐ効かせるなら(といってもプロンプトに出すにはシェル起動が必要)hostnameコマンドで。

# hostname new_host_name.example.com

システムのタイムゾーン

  • ユーザ毎の設定で上書きできるけれど、一応システムのタイムゾーンを設定しておく。

# ln -s /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

ipfw

  • 何はともあれ早めにipfwの設定を入れる。というのは、失敗するとログインができなくなって(AWS EC2ではコンソールは見えても入力できないので)手も足も出ずインスタンス再作成に追い込まれるから。
  • ipfwは暗黙のdefaultルールがdeny allなので、いきなり有効にしては駄目。
  • rc.confにfirewall="YES"があれば内部でkldload ipfw.koをやってくれるのでloader.confに明記する必要はないが、書くなら/boot/loader.conf.localで

ipfw_load="YES"

  • /etc/rc.conf.localで

firewall_enable="YES"

firewall_script="/etc/ipfw.rules"

  • /etc/ipfw.rulesで

#!/bin/sh

IPFWQ="/sbin/ipfw -q"

 

### flush old rules

$IPFWQ -f flush

# $IPFWQ -f nat flush

$IPFWQ table all destroy

 

### OK list of source ip addresses

$IPFWQ table T_GOODIP create

$IPFWQ table T_GOODIP add <trusted ip address>/32

 

### NG list

$IPFWQ table T_BADIP create

$IPFWQ table T_BADIP add <rogue ip address>/32

 

### ALLOW ALL VIA LOOPBACK

$IPFWQ add 10 allow ip from any to any via lo0

#$IPFWQ add 11 deny ip from any to 127.0.0.0/8

#$IPFWQ add 12 deny ip from 127.0.0.0/8 to any

 

### LIMIT SRCIP to important ports

$IPFWQ add 100 allow ip from table\(T_GOODIP\) to me dst-port 10022

$IPFWQ add 101 deny ip from any to me dst-port 10022

 

### DENY BAD SRCIP and BAD DSTPORT.

$IPFWQ add 60000 deny ip from table\(T_BADIP\) to any

 

### permissive catch-all rule.

$IPFWQ add 65000 allow ip from any to any

 

### DEFAULT DENY added by default.

# 65535 deny ip from any to any

  • ipfwを有効にする。ま、ちょっと落ち着いて上の設定をよく確認するほうがよい。default denyなので、失敗するとsshのログインができないどころか、今コマンドを投入したこのセッションすら叩き落されます。
# service ipfw status
# service ipfw start
  • ipfwルール設定確認は

# ipfw list

  • 信頼してよい踏み台ノードなどのIPアドレスをT_GOODIPに入れておく。
  • spammerなんかが面倒な時はT_BADIPに入れておく

# ipfw table T_GOODIP list

# ipfw table T_GOODIP add <trunsted ip>/32

# ipfw table T_BADIP add <spammer ip>/24

  • 後でsshdでblacklistdを使うので、blacklistdに対してfirewallとしてはipfwを使えと設定しておく。
# touch /etc/ipfw-blacklist.rc
  • 2020/Sep/29追記:ipfwのdefault denyをdefault allowにするには、/boot/loader.conf.localで次のtunableを設定すればよい。

net.inet.ip.fw.default_to_accept=1

  • ↑はFreeBSDワークショップ#67のチャットで教えていただきました。#68はOct/30!

 

初回reboot

  • service ipfw startの後もコマンドラインの操作ができていれば大丈夫なはずだが、ipfwの設定を間違ってないことを確認するため、ここで初回reboot。
  • もしSSHアクセスができなくなったら、別のVMを建ててこのVMのディスクイメージを/mntあたりにマウントして設定ミスを修正する手段はあるけど、多分、VMを捨てて作り直す方が早い。

共通設定

sshdオプション

  • パスワード認証をさせない、rootログインをさせない、blacklistdを使う、その他の設定。
  • portを変更しておくと、port scanが相当減るのでおすすめ。(下の例では10022に変更)
 
  • /etc/rc.conf.localで

sshd_enable="YES"

sshd_flags="-o ClientAliveCountMax=8 -o ClientAliveInterval=15 -o ListenAddress=0.0.0.0:10022 -o PasswordAuthentication=no -o PermitRootLogin=no -o Protocol=2 -o TCPKeepAlive=yes -o UseDNS=no -o UsePam=no -o X11Forwarding=yes -o KbdInteractiveAuthentication=no -o UseBlackList=yes"

blacklistd_enable="YES"

blacklistd_flags="-f"

  • sshdプロセス再起動で有効になる。外からsshでログインできて、su -でroot権限を取れるところまで念の為確認。

# service sshd status

# service sshd restart

  • (2020/Sep/29 場所の移動) ついでにblacklistdも起動。これで、sshdで認証失敗を経験したらblacklistdに伝達し、blacklistdでの閾値(個別に設定もできる)を越えたらipfwに拒否ルールを突っ込むという動きになるはず。

# service blacklistd status

# service blacklistd start

resolv.conf設定確認

  • 名前解決のためのresolv.confもCollins先生が設定済。
  • 172.31.0.2はAWS EC2側が用意しているリゾルバーなので、特に理由がなければこれで十分。
  • search domainは変更しても良いかもしれない。

# cat /etc/resolv.conf

# Generated by resolvconf

search us-west-2.compute.internal

nameserver 172.31.0.2

  • (2020/Sep/29追記) search domainを変更するなら/etc/resolvconf.confでこんな感じ。それにしてもこの設定ファイルの名前はかなりびみょー。

search_domains="example.com example.jp"

ntpd設定確認

  • Collins先生のマシンイメージからインストールしているので、AWS EC2でのntpサーバ169.254.169.123に同期する設定がすでに入っている。

# ntpq -p

remote                 refid               st t when poll reach   delay   offset  jitter

===================================================

*169.254.169.123 10.79.71.196  3 u  537 1024  377 0.306   +0.443   0.079

/etc/aliases

  • /etc/aliasesでroot宛のメールを適当な受信者へaliasしておく。

root: someone@real.mail.reader

/etc/periodic.conf.local

  • /etc/periodic.conf.localでシステム管理用の定期起動モノの調整。

# /etc/periodic.conf.local

 

daily_clean_tmps_enable="YES"

daily_news_expire_enable="NO"

daily_status_ntpd_enable="YES"

daily_status_security_inline="YES"

daily_status_smart_devices="/dev/ada0"

 

weekly_noid_enable="YES"

weekly_show_badconfig="YES"

weekly_status_security_inline="YES"

 

monthly_status_security_inline="YES"

 

security_show_badconfig="YES"

pkgコマンド

  • よく使うプログラムをインストールするのにpkgコマンドを使う。

# pkg update

# pkg search <package name pattern>

# pkg install <package name>

# pkg upgrade [<package name>]

# pkg delete <package name>

bash

  • (2020/Sep/29追記)
  • bash入れるの忘れてました。

# pkg install bash

doas

  • security/sudoでも良いが、security/doasの方がスパルタン。なんせOpenBSD由来。
  • sudoだと一回認証されればしばらくの間は認証なしに使えるが、doasでそれができるのはOpenBSDで使ったときだけ(泣)
  • でも(ネットで見た偉い人のお言葉によると)sudoはちょっと肥大化してきたのでバグ混入も心配しなきゃいけないかも。その点doasはコンパクトやし、あのOpenBSD出身やし。
  • インストールは

# pkg search doas

doas-6.3                       Simple sudo alternative to run commands as another user

# pkg install doas

 

# echo "permit persist :wheel" >> /usr/local/etc/doas.conf

lv

  • moreだとjkで上下するとかできない(よね?)のでついついmisc/lv入れちゃう。
  • 日本語も通るし。

# pkg search lv-4.51

lv-4.51_3                      Powerful Multilingual File Viewer

# pkg install lv

nkf

  • 日本語といえばとりあえずjapanese/nkf

# pkg search ja-nkf

ja-nkf-2.1.4,1                 Network Kanji code conversion Filter

# pkg install ja-nkf

lftp

  • 近頃とんと使わなくなったけれど、anonymous FTPの時に入力少なくて楽なftp/lftp

# pkg search lftp-4.9.1

lftp-4.9.1                     Shell-like command line FTP client

# pkg install lftp-4.9.1

vim

  • /bin/viでもいいんだけどやっぱり便利なeditors/vim
  • でもX Window Systemは使う予定もないので、editors/vim-liteで十分だよねと思ったら、なくなっていた。
  • FreeBSD Forumとか/usr/ports/UPDATINGとか見ると、元々X対応なvimパッケージと、コンソール限定の(ということはX対応を削った)vim-liteがあったが、vim-liteがちっとも「軽く」ないので名称変更してeditors/vim-consoleになったとのこと。

     

  • じゃeditors/vim-tinyは何?っていうと、/usr/ports/editors/vim-tiny/pkg-descrの後半に、コンソール限定で、かつvimバイナリだけにした(からヘルプやシンタックスその他ランタイムのファイルを削った)ミニマムインストール、と書いてある。

This is the "tiny" version, which is console-only and contains ONLY the vim

binary. It contains no help files, syntax files, or any other runtime files,

and is designed only for minimal installs. You almost always want the vim

or vim-console package instead.

  • Xは不要だけどsyntaxは欲しいかなということで、vim-consoleをインストールしておく。

# pkg search vim-console

vim-console-8.2.1110           Improved version of the vi editor (console only)

[root@HarbinCafe0 /usr/home/moto]# pkg install vim-console

git

  • あと、まあ、いらない人はいらないと思うけど、gitコマンド。
  • これもX対応を含めて依存関係の大きいdevel/gitを避けてdevel/git-liteにする。
  • pkg-descrに"This version provides the bare minimum git experience without any bindings."とあるので、bindingsが要るようになったらdevel/gitにしよう。

# pkg search git-lite

git-lite-2.27.0                Distributed source code management tool (lite package)

# pkg install git-lite

  • (2020/Sep/29追記)gitパッケージがX11関係のライブラリを大量にインストールすると思い込んでましたが違いました。でもまあperl5やpython37にgnupg、あと何故かsubversionも突っ込むので考えどころではあります。

/etc/profile + ~/.bashrc + ~/.bash_profile

  • 基本的な環境変数やaliasを/etc/profileに入れておく。

alias cp='cp -i'

alias ftp='lftp'

alias less='lv'

alias ls='ls -FG'

alias more='lv'

alias mv='mv -i'

alias rm='rm -i'

alias sudo=doas

alias tac='tail -r'

alias vi='vim'

alias view='vim -R'

 

EDITOR=vim;        export EDITOR

PAGER=lv;        export PAGER

LANG=ja_JP.UTF-8;    export LANG

LC_MESSAGES=C;        export LC_MESSAGES

LC_TIME=C;        export LC_TIME

TMOUT=1200;        export TMOUT

PS1="[\u@\h \W]\\$ "     export PS1

 

MANPATH=${HOME}/local/man:/usr/local/man:/usr/share/man

export MANPATH

 

PATH=${HOME}/local/sbin:${HOME}/local/bin

PATH=${PATH}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

export PATH

  • これで、ログインユーザとして(bash的には-l付き)bashを起動するとaliasと環境変数が有効になる。
  • ところが、sshログインでログインシェルのbashを起動したような時に、/etc/profileを読んだあとホームディレクトリの.bash_profile, .bash_login, .profileをこの順に探索して最初に存在したファイルを読み込むので、デフォルトで置かれる.profileを読んでしまう。ここにEDITOR=viとかあるのでこれは避けたい。そこで、空のファイルで良いので~/.bash_profileを作っておくことにした。

$ touch ~/.bash_profile

  • これでもログインしたあとにコマンドラインからbashを起動するとaliasが抜けるが、bash -lとすることで運用対処。
  • 他のシェルとの兼ね合いとかあったらどうするんだろうね。

まとめ

  • 殴り書きだが一応FreeBSDのノードを建てたらこの辺まではやっておくという備忘録を作った。
  • 書けば書くほどあれはどうするこれはどうすると疑問が膨らむ。
  • パーティション構成とかファイルシステムのRead Onlyマウント、/boot/loader.confや/etc/sysctl.confの整理もこれからの課題。

おまけ

  • ちょっとだけ/etc/sysctl.conf.localで調整。

# /etc/sysctl.conf.local

 

# kern.randompid: Random PID modulus. Special values: 0: disable, 1: choose random value

# kern.randompid: 0

kern.randompid=1

 

# net.inet.icmp.drop_redirect: Ignore ICMP redirects

# net.inet.icmp.drop_redirect: 0

net.inet.icmp.drop_redirect=1

 

# net.inet.ip.check_interface: Verify packet arrives on correct interface

# net.inet.ip.check_interface: 0

net.inet.ip.check_interface=1

 

# net.inet.ip.random_id: Assign random ip_id values

# net.inet.ip.random_id: 0

net.inet.ip.random_id=1

 

# net.inet.ip.redirect: Enable sending IP redirects

# net.inet.ip.redirect: 1

net.inet.ip.redirect=0

 

# net.inet.sctp.blackhole: Enable SCTP blackholing, see blackhole(4) for more details

# net.inet.sctp.blackhole: 0

net.inet.sctp.blackhole=2

 

# net.inet.tcp.blackhole: Do not send RST on segments to closed ports

# net.inet.tcp.blackhole: 0

net.inet.tcp.blackhole=2

 

# net.inet.tcp.drop_synfin: Drop TCP packets with SYN+FIN set

# net.inet.tcp.drop_synfin: 0

net.inet.tcp.drop_synfin=1

 

# net.inet.tcp.ecn.enable: TCP ECN support

# net.inet.tcp.ecn.enable: 2

# 0: disabled, 1: enabled for send/recv, 2: enabled for recv

net.inet.tcp.ecn.enable=1

 

# net.inet.tcp.fast_finwait2_recycle: Recycle closed FIN_WAIT_2 connections faster

# net.inet.tcp.fast_finwait2_recycle: 0

net.inet.tcp.fast_finwait2_recycle=1

 

# net.inet.tcp.finwait2_timeout: FIN-WAIT2 timeout

# net.inet.tcp.finwait2_timeout: 60000

net.inet.tcp.finwait2_timeout=3000

 

# net.inet.tcp.icmp_may_rst: Certain ICMP unreachable messages may abort connections in SYN_SENT

# net.inet.tcp.icmp_may_rst: 1

net.inet.tcp.icmp_may_rst=0

 

# net.inet.tcp.nolocaltimewait: Do not create compressed TCP TIME_WAIT entries for local connections

# net.inet.tcp.nolocaltimewait: 0

net.inet.tcp.nolocaltimewait=1

 

# net.inet.tcp.per_cpu_timers: run tcp timers on all cpus

# net.inet.tcp.per_cpu_timers: 0

net.inet.tcp.per_cpu_timers=1

 

# net.inet.tcp.recvbuf_auto: Enable automatic receive buffer sizing

# net.inet.tcp.recvbuf_auto: 1

#net.inet.tcp.recvbuf_auto=1

 

# net.inet.tcp.rfc1323: Enable rfc1323 (high performance TCP) extensions

# net.inet.tcp.rfc1323: 1

#net.inet.tcp.rfc1323=1

 

# net.inet.tcp.sack.enable: Enable/Disable TCP SACK support

# net.inet.tcp.sack.enable: 1

#net.inet.tcp.sack.enable=1

 

# net.inet.tcp.tso: Enable TCP Segmentation Offload

# net.inet.tcp.tso: 1

net.inet.tcp.tso=0

 

# net.inet.udp.blackhole: Do not send port unreachables for refused connects

# net.inet.udp.blackhole: 0

net.inet.udp.blackhole=1

 

# security.bsd.hardlink_check_gid: Unprivileged processes cannot create hard links to files owned by other groups

# security.bsd.hardlink_check_gid: 0

security.bsd.hardlink_check_gid=1

 

# security.bsd.hardlink_check_uid: Unprivileged processes cannot create hard links to files owned by other users

# security.bsd.hardlink_check_uid: 0

security.bsd.hardlink_check_uid=1

 

# security.bsd.see_jail_proc: Unprivileged processes may see subjects/objects with different jail ids

# security.bsd.see_jail_proc: 1

security.bsd.see_jail_proc=0

 

# security.bsd.see_other_gids: Unprivileged processes may see subjects/objects with different real gid

# security.bsd.see_other_gids: 1

security.bsd.see_other_gids=0

 

# security.bsd.see_other_uids: Unprivileged processes may see subjects/objects with different real uid

# security.bsd.see_other_uids: 1

security.bsd.see_other_uids=0

 

# security.bsd.stack_guard_page: Specifies the number of guard pages for a stack that grows

# security.bsd.stack_guard_page: 1

#security.bsd.stack_guard_page=1

 

# security.bsd.unprivileged_proc_debug: Unprivileged processes may use process debugging facilities

# security.bsd.unprivileged_proc_debug: 1

security.bsd.unprivileged_proc_debug=0

 

# security.bsd.unprivileged_read_msgbuf: Unprivileged processes may read the kernel message buffer

# security.bsd.unprivileged_read_msgbuf: 1

security.bsd.unprivileged_read_msgbuf=0

  • あ、/boot/loader.conf.localもほんの少しだけ。

# /boot/loader.conf.local

 

accf_dns_load="YES"

はじめに

  • PostgreSQLは有名なDBMS。Open SourceでFreeなDBMSでいうとこれかMariaDBあたりが挙がるんじゃないかと思う。僕はこっちの方がなにかとかっちりしていて好きなので使っているけど、まあ、あまり詳しいわけじゃない。
  • 今動かしているのはPostgreSQL11。すでに12が出ていて13も時間の問題で出るのでバージョンアップしないといけないけれど、それはまた別の話。それから、多分今回は関係ないけどOSにはFreeBSD 12-RELEASEを使っている。(FreeBSDはいいぞ
  • PostgreSQLにはinet型とかcidr型があって一応IPアドレスやCIDRブロックを記録することができる。IPアドレス/CIDRブロック特有の演算子や関数もある。
  • ところが、組み込みのものはインデックスを貼っても「すごく遅い」し、ふたつの型が「意味的にうまく切り分けられていない」(違いはinet型なら'127.0.0.1/8'と書けるがcidr型では駄目というだけ)と主張する方がおられて、ip4rなるEXTENSIONを公開しておられる。
何がしたいの?
  • いずれにしてもPostgreSQLでもってCIDRブロックをうまく扱いたいわけだが、大小さまざまなCIDRブロックを重ならないように一列に収めることを考える。要するにUNIQUEではできずにEXCLUDEでやる必要があることがわかったよというのがこのページの主旨。(他にもっとうまいやり方があったら是非教えて下さいませ。)
UNIQUEの例(失敗)
  • なにはともあれ素直にやってみる。iprange型(ip4rの方でIPv4かIPv6のCIDRを収容する型ですね)にUNIQUE制約を付けて適当なテーブルを作成する。

CREATE TABLE public.foo (

  range iprange UNIQUE

);

CREATE TABLE

Time: 6.584 ms

  • ところが、これだと全く同じふたつのCIDRは弾いてくれるが、

INSERT INTO public.foo

  (range)

  VALUES

  ('127.0.0.0/8'), ('127.0.0.0/8');

ERROR:  duplicate key value violates unique constraint "foo_range_key"

DETAIL:  Key (range)=(127.0.0.0/8) already exists.

Time: 0.730 ms

  • 相重なる2つのCIDRが入ってしまうのだ。

INSERT INTO public.foo

  (range)

  VALUES

  ('127.0.0.0/8'), ('127.0.0.0/16');

INSERT 0 2

Time: 0.689 ms

 

SELECT * FROM public.foo;

    range     

--------------

 127.0.0.0/8

 127.0.0.0/16

(2 rows)

 

Time: 0.259 ms

ではどうする?
  • それでは困るってことで、テーブルの列に付ける制約を探してみるとCREATE TABLEにEXCLUDEなるものがあって、これだと行けそう。先にCHECK制約を考えたんですが、こちらはその一行の中での条件を書くことはできるが、前後の(既存の全ての)行との間の関係は書けないようだ。
  • 今やりたいのは上記の127/8と127/16のように包含関係になっているものは書けないようにするってことだから、ip4rの&&演算子で返されるbooleanを見て判断すれば良いよね。

SELECT '127.0.0.0/8'::iprange && '127.0.0.0/8'::iprange AS overlap_p;

 overlap_p 

-----------

 t

(1 row)

 

Time: 0.162 ms

 

SELECT '127.0.0.0/8'::iprange && '127.0.0.0/16'::iprange AS overlap_p;

 overlap_p 

-----------

 t

(1 row)

 

Time: 0.106 ms

 

SELECT '127.0.0.0/8'::iprange && '10.0.0.0/8'::iprange AS overlap_p;

 overlap_p 

-----------

 f

(1 row)

 

Time: 0.097 ms

EXCLUDEの例(成功)
  • そうすると、UNIQUE制約ではなくてEXCLUDE制約で縛ることにして、その条件は&&演算子で同列他行のデータとの間にCIDRブロック的包含関係がない(fが返る)ことを担保すればよいから、こうなる。

CREATE TABLE public.bar (

  range iprange

 ,EXCLUDE USING GIST (range WITH &&)

);

CREATE TABLE

Time: 5.678 ms

  • これで先ほどと同じ例を突っ込んでみると、まず、全く同じふたつのCIDRは弾かれる。これはさっきもできていた。

INSERT INTO public.bar

  (range)

  VALUES

  ('127.0.0.0/8'), ('127.0.0.0/8');

ERROR:  conflicting key value violates exclusion constraint "bar_range_excl"

DETAIL:  Key (range)=(127.0.0.0/8) conflicts with existing key (range)=(127.0.0.0/8).

Time: 0.666 ms

  • mask長を変えて、でも重なるふたつのCIDRについては、さっきは挿入できてしまったが今回は意図に添って弾いてくれる。

INSERT INTO public.bar

  (range)

  VALUES

  ('127.0.0.0/8'), ('127.0.0.0/16');

ERROR:  conflicting key value violates exclusion constraint "bar_range_excl"

DETAIL:  Key (range)=(127.0.0.0/16) conflicts with existing key (range)=(127.0.0.0/8).

Time: 0.273 ms

まとめ
  • というわけで、PostgreSQLでCIDRブロックが重ならないように制約することができた。