ksh

Table of Contents

P2P

前回、唐突にウェブサーバーを立てた。究極の目的は、IPv6なら、面倒な事なく、 ネットワーク間でアクセスできると思ったから。でも….

Q: OpenBSD httpd で起動したサーバーに、別なネットワークからアクセス出来ません。 チェックポイントを教えてください。

inet6 2401:… prefixlen 64 autoconf temporary (server side) こちらでサーバーを起動。

fu$ cat /etc/httpd.conf
server "example.com" {
    listen on * port 80
}

fu$ w3m http://[2401…]/ipv6/ で、起動を確認済み。

inet6 2400:… prefixlen 64 autoconf temporary (client side) 別なネットワークから ob$ w3m http://[2401…]/ipv6/ すると、接続できずにタイムアウトしてしまう。

なお両ネットワーク共、一般家庭用に提供されているものです。 IPv6なら、何の制限も無しにPtoPで接続できるものと思っていたので、質問する 次第です。

A: なるほど、状況よくわかりました。 OpenBSD httpd 自体は LAN 内から確認できているので、サーバープロセスや設定は正しく動いています。外部ネットワークからつながらない場合は、以下のようなチェックポイントを順番に確認すると切り分けしやすいです。


## 1. サーバー側 (OpenBSD) の確認

  • pf(4) のフィルタルール
    • OpenBSD は pf がデフォルトで有効な場合があります。
    • 外部からの tcp port 80 を許可するルールがあるかを確認:
# pfctl -sr
  • 無ければルールを追加(例):
pass in on egress inet6 proto tcp to port 80

追加後に pfctl -f /etc/pf.conf で反映。

  • リッスン確認
    • ちゃんと v6 アドレスで待ち受けているか:
# netstat -an | grep LISTEN | grep 80

:::80[2401:...] が出ているかを確認。


## 2. ネットワーク経路の確認

  • 外からの疎通確認
    • クライアントから:
ping6 [2401:...]
traceroute6 [2401:...]
  • 途中で止まる場合 → ISP 側やルータ側の制限の可能性。
  • サーバー自身から外に出られるか
ping6 google.com

IPv6 経路が正しく張れているかを確認。


## 3. IPv6 アドレスの種類を確認

  • temporary アドレス
    • autoconf temporary は「一時的なプライバシーアドレス」です。
    • サーバー用途には不向き(数時間~数日で変わる)。
    • 外部からアクセスするには autoconf の固定アドレス(EUI-64 など)や、ルータ配布の stable アドレスを使うのが無難です。

## 4. ルータ/ISP 側の仕様

  • 家庭用 ISP の制限
    • IPv6 は基本的に end-to-end 通信できるはずですが、家庭用回線では inbound(外から内への接続)がブロックされていることがあります。
    • 代表的には:
      • ルータの IPv6 ファイアウォール機能 で 80/TCP が塞がれている
      • ISP 側で inbound を遮断している(特に v6 プライバシー保護のため)
    • ご利用のルータの設定画面に「IPv6 ファイアウォール」「IPv6 ポート開放」などの項目があれば確認してください。

## 5. 切り分けのヒント

  • 同じサーバーに対して、/LAN 内の別端末から IPv6 アドレスでアクセスできるか/をまず確認。
    • できる → ISP/ルータ問題
    • できない → pf/httpd 設定の問題

### まとめ

  • httpd の設定自体は問題なし。
  • まずは pf の許可ルール確認 と /temporary ではない v6 アドレスの利用/。
  • その後、/ルータの IPv6 ファイアウォール/ポート開放設定/を確認。
  • もしそれでもダメなら ISP 側が inbound IPv6 を制限している可能性大です。

test 1

まずはサーバー側のパケットフィルター設定

fu$ doas pfctl -sr
pass in on egress inet6 proto tcp from any to any port = 80 flags S/SA
block return all
pass all flags S/SA
block return in on ! lo0 proto tcp from any to any port 6000:6010
block return out log proto tcp all user = 55
block return out log proto udp all user = 55

追加したのは、主要なI/F(egress)からのtcp6/80ポートは通行許可します。

fu$ doas netstat -an | grep LISTEN | grep 80
tcp          0      0  *.80                   *.*                    LISTEN
tcp6         0      0  *.80                   *.*                    LISTEN

待受状況にtcp6/80ならどんなIPでも受信しますってサイン。

次はクライアント側からサーバーに導通するか確認。IPは伏せ字にしてます。

ping6 -c 3 2401:...
64 bytes from 2401:...: icmp_seq=0 hlim=59 time=23.928 ms
64 bytes from 2401:...: icmp_seq=1 hlim=59 time=20.749 ms
64 bytes from 2401:...: icmp_seq=2 hlim=59 time=24.342 ms

こんな状況にもかかわらず、w3mでの閲覧はかないませんでした。やっぱり プロバイダーがルーターの中で関所を設けているのかなあ。

サーバー側のネットワークは長屋の人々が共用する設備。そんなネットワークに IPv6限定とはいえ、勝手にウェブサーバーを立てられて(オイラーのような人ね)、 もしそのサーバーが大ヒットしちゃったら、長屋の住民は迷惑を被る。 プロバイダーとしては、是非これを防止したいんだな。

test 2

だったら、サーバーとクライアントの役目をネットワーク上で反転したらどうだろう。 ひょっとしたら、プロバイダーのポリシーの違いで、上手くいくかも。

長屋ネットワーク(2401:…)をクライアント、既存の個人契約ネットワーク(2400:…) をサーバーにしてみる。

fu$ ping6 -c 3 2400:...
64 bytes from 2400:... icmp_seq=0 hlim=59 time=23.887 ms
64 bytes from 2400:... icmp_seq=1 hlim=59 time=29.408 ms
64 bytes from 2400:... icmp_seq=2 hlim=59 time=135.644 ms

クライアント側からサーバー目掛けて導通試験。逆にサーバー側からクライアント に向けても導通は有るはずだな(未確認だけど)。

fu$ traceroute6 2400:...
traceroute6 to 2400:... (2400:...), 64 hops max, 60 byte packets
 1  2401:... (2401:...)  3.507 ms  1.513 ms  1.274 ms
 2  2401:... (2401:...)  3.303 ms  2.599 ms  5.059 ms
 3  * * *
 4  * * *
 5  2400:... (2400:...)  10.467 ms  13.057 ms  10.165 ms
 6  2400:... (2400:...)  10.903 ms  10.827 ms  13.588 ms

トレース試験も、堂々と実施できるな。こんなホップで届いてしまうんですかね。 本当かどうか、ググルあたりに対して実験したら、通報されちゃったりして。

example.com 2400:... - - [28/Sep/2025:06:47:37 +0900] "GET /ipv6 HTTP/1.0" 301 0
example.com 2400:... - - [28/Sep/2025:06:47:37 +0900] "GET /ipv6/ HTTP/1.0" 200 138
example.com 2401:... - - [28/Sep/2025:07:00:26 +0900] "GET /ipv6 HTTP/1.0" 301 0
example.com 2401:... - - [28/Sep/2025:07:00:26 +0900] "GET /ipv6/ HTTP/1.0" 200 138
example.com 2401:... - - [28/Sep/2025:07:07:39 +0900] "GET /ipv6 HTTP/1.1" 301 0
example.com 2401:... - - [28/Sep/2025:07:07:39 +0900] "GET /ipv6/ HTTP/1.1" 200 138
example.com 2401:... - - [28/Sep/2025:07:07:40 +0900] "GET /favicon.ico HTTP/1.1" 404 0

そしてサーバーのログも確認。2400:… はLubuntuのw3mから、同一ネットワーク上に あるOpenBSD httpd への接続。次からの2401:… は、FuguIta 7.7 から、w3m及び Firefoxでの接続。firefoxって、faviconも要求するんですねぇ。

こうして、P2Pの実験は成功しました。やっぱりプロバイダーによっては制限を 設けているんですね。中々、教科書通りにはいかないっていう、良い体験をしました。

todo

Q: 簡単にネットワーク実験をするにはどんな手段が有りますか?

A: なるほど!プロバイダー側で inbound IPv6 を遮断していた、ということが確認できたんですね 👍 実験環境では ISP 制限を避けるために「完全に自分で制御できる仮想ネットワーク」を立てるのが王道になります。

以下のような選択肢があります:


## 1. ネットワークシミュレーション系ディストリビューション

  • /GNS3/(Linux 上で利用) Cisco/Juniper/FRRouting などのルータ仮想機器を並べて実験可能。 実際の IP スタックでルーティング・FW・NAT・IPv6 実験ができる。
  • EVE-NG GNS3 と同様の用途で、複雑な仮想ネットを GUI で構築できる。

## 2. 軽量ルータ系 OS を組み合わせて自作

  • VyOS Debian ベースのルータディストリ。IPv6、BGP/OSPF、FW/NAT、VPN、仮想ネットワーク構築に強い。 → 普通の PC 上に複数 VM を立て、仮想スイッチでつなげば「小さなインターネット」を作れる。
  • OpenBSD / FreeBSD pf / ipfw を駆使して自前のルータ・FW環境を組める。 → 既に OpenBSD をお使いなので、サーバーとルータ VM を分けて仮想環境を作るのも手。

## 3. 仮想環境の基盤

  • QEMU/KVM + Linux bridge QEMU の内部ネットワーク(=-netdev user=, =-netdev tap=)を使えば ISP に依存せず実験可能。 → ルーティングや IPv6 アドレス設計も自由にできる。
  • VirtualBox / VMware 「ホストオンリーアダプタ」「内部ネットワーク」を使えば、外界に出ない閉じた実験用 IPv6 ネットを作れる。
  • Mininet SDN 研究用。仮想スイッチ+仮想ホストを Python スクリプトで即座に構築できる。 → トポロジを大量に作るなら楽。

## 4. 実験用 IPv6 ネットの作り方のイメージ

  • ルータ VM(VyOS/OpenBSD/FRR)を立てる
  • サーバー VM とクライアント VM を立ててルータに接続
  • IPv6 プレフィックスは =fd00::/8=(ULA = Unique Local Address)を自由に割り当て
  • ルータに NAT66 や FW 設定を入れて挙動をテスト
  • 外界の ISP には一切依存しないので、ポート制限や FW に悩まされない

✅ まとめると、 「制限を一切考えずに IPv6/PF/HTTP 実験したい」なら /QEMU/KVM 上に複数 VM を立てて VyOS や OpenBSD をルータにする/のが一番シンプルです。 もっと GUI で柔軟にネット構築したいなら /GNS3/EVE-NG/。

ksh

前回ちょっと触れたkshについて調査。typesetなんて言う備えつきのコマンドなんて 知らなかったぞ。

typeset

OpenBSDのkshの正体。

ad$ echo $KSH_VERSION
@(#)PD KSH v5.2.14 99/07/13.2

199X年の頃はkshが自由に使える時代ではなかった。そこでパブリックドメイン版の kshを使う事にした。shはOpenBSDの生命線。/etc/rcは言うに及ばず、インソトーラー もshで記述されてる。根幹は変更するなって事で、ずっとこのパブリック版を保守 し続けている。頑固一徹。どこかのLinuxみたいな尻軽さんではないのです。 勿論、最新?のkshはパッケージになって提供されている。

FreeBSDのksh93に、その辺の事が記述されてたぞ。

ksh93u+m is the ksh93 reboot aims to develop bugfixes to the last
stable release (93u+ 2012-08-01) of ksh93, formerly developed by
AT&T Software Technology (AST). This ksh 93u+m reboot now incorporates
many of these bugfixes, plus patches from OpenSUSE, Red Hat, and Solaris,
as well as many new fixes from the community.

Between 2017 and 2020 there was an ultimately unsuccessful attempt to
breathe new life into the KornShell by extensively refactoring the last
unstable AST beta version (93v-). While that ksh2020 effort is now
abandoned and still has many critical bugs, it also had a lot of bugs
fixed. More importantly, the AST issue tracker now contains a lot of
documentation on how to fix those bugs, which made it possible to
backport many of them to the last stable release instead.

どんな風な挙動をするか? 引数無しだと、変数(関数)の属性表示をしてくれた。

ad$ typeset
typeset COLUMNS
typeset -x HOME
typeset IFS
typeset -r KSH_VERSION
typeset -x LANG
typeset -x PATH
typeset -i -r PPID
 :

-x はエクスポート。-r はリードオンリー。-i は整数扱い。

rsh/sh/ksh

kshのMakefileに、こんな設定が記述されてた。

LINKS=  ${BINDIR}/ksh ${BINDIR}/rksh
LINKS+= ${BINDIR}/ksh ${BINDIR}/sh

大体、こういうのの振り分けは、mainに記述されてるんだよな(こういう決め付けは、 厳に慎しめ。最初は間口を広くが常識です)。

ad$ grep strcmp main.c
        if (!strcmp(kshname, "sh") || !strcmp(kshname, "-sh") ||
            !strcmp(&kshname[strlen(kshname) - 3], "/sh"))) {
        if (strcmp(name, "rsh") && \
                strcmp(name, "rksh") && \
                strcmp(name, "rpdksh") && \
                strcmp(name, "pdrksh"))

ちょっと潜ってみると、

/* Returns true if name refers to a restricted shell */
static int
is_restricted(char *name)
{
        char *p;

        if ((p = strrchr(name, '/')))
                name = p + 1;
        /* accepts rsh, rksh, rpdksh, pdrksh */
        if (strcmp(name, "rsh") && \
                strcmp(name, "rksh") && \
                strcmp(name, "rpdksh") && \
                strcmp(name, "pdrksh"))
                return(0);
        else
                return(1);
}

機能の制限付きの検出ですか。

/* Check to see if we're /bin/sh. */
if (!strcmp(kshname, "sh") || !strcmp(kshname, "-sh") ||
    (strlen(kshname) >= 3 &&
    !strcmp(&kshname[strlen(kshname) - 3], "/sh"))) {
        Flag(FSH) = 1;

もう一つの視点は sh か否かの検出だな。こういうのは、一度内部フラグに変換 しておいて使用される。たとえば

if (restricted) {
        static const char *const restr_com[] = {
                "typeset", "-r", "PATH",
                "ENV", "SHELL",
                NULL
        };

こんな具合に制限付きの場合、PATHを書き換え不可にして、行動範囲を限定しちゃう とか。詳細はksn(1)だな。いやsh(1)もscriptを書きく場合は必読だな。kshのそれより ずっと簡潔に説明されてる。

typeset with gdb

typesetはkshの起動時にも積極的に利用されてる(変数のexportとかね)。素直に typesetにBPを置いてgdbすると、何回もbreakしちゃって鬱陶しい。 そこで、最初にgdb付きのkshを起動しといてから、gdbをアタッチしてみる。

vm$ gdb -q ksh -p 48713
Reading symbols from ksh...
Attaching to program: /tmp/ksh/ksh, process 48713
Reading symbols from /usr/lib/libcurses.so.15.0...
Reading symbols from /usr/lib/libc.so.100.3...
Reading symbols from /usr/libexec/ld.so...
_thread_sys_read () at /tmp/-:2

warning: 2      /tmp/-: No such file or directory
(gdb) b typeset
Breakpoint 1 at 0xa0e9190de5d: file var.c, line 607.
(gdb) c
Continuing.

Breakpoint 1, typeset (var=0xa0e918c7ede "COLUMNS", set=0, clr=0, field=0,
    base=0) at var.c:607
607             val = skip_varname(var, false);

別端末のkshからコマンドを投入したよ。

vm$ typeset -r zlock="mylock"

replなんで、どのあたりを回っているかの確認が基本です。

(gdb) bt
#0  typeset (var=0xa0e918c7ede "COLUMNS", set=0, clr=0, field=0, base=0)
    at var.c:607
#1  0x00000a0e918d9758 in check_sigwinch () at edit.c:81
#2  0x00000a0e918d9896 in x_read (
    buf=0xa111d132010 "typeset -r zlock=\"mylock\"\n", '\337' <repeats 173 times>, <incomplete sequence \337>..., len=4096) at edit.c:111
#3  0x00000a0e918fa8d9 in getsc_line (s=0xa10bd8bf990) at lex.c:1099
#4  0x00000a0e918fa454 in getsc__ () at lex.c:988
#5  0x00000a0e918f7f2f in getsc_bn () at lex.c:1659
#6  0x00000a0e918f46d1 in yylex (cf=44) at lex.c:167
#7  0x00000a0e91904d93 in get_command (cf=0) at syn.c:206
#8  0x00000a0e91904b88 in pipeline (cf=0) at syn.c:77
#9  0x00000a0e9190493c in andor () at syn.c:98
#10 0x00000a0e9190449d in c_list (multi=0) at syn.c:118
#11 0x00000a0e919043b7 in yyparse () at syn.c:64
#12 0x00000a0e91904352 in compile (s=0xa10bd8bf990) at syn.c:769
#13 0x00000a0e918fc7b4 in shell (s=0xa10bd8bf990, toplevel=1) at main.c:628
#14 0x00000a0e918fbf98 in main (argc=1, argv=0x7927d11bb398) at main.c:452

この関数が、数度callされてたぞ。もう少し詳細を追跡する必要が有りますね。

nbld by ksh

kshに触れたんで、何か記念品を作成したいな。で、あれですよ。

血圧グラフ(nbld.go)

色々な言語で記述した事があったけど、shell scriptは、まだだった。 さわりだけでも書いてみるかな。

一番多用する、部分。チラ裏に測定結果をメモしておいて、それを10日分 ぐらい入力。以前のデータに追加。そしたらそれを70日分取り出してきて 朝夜に分離、gnuplotでグラフにする。

入力したレコードを、新らしいファイルにappend。そのファイルと既存の ファイルをcatしてあげればいいんだなとラフな構想をした。 まっさきにやるのは、1レコードの入力。当然、関数化する。

#! /bin/sh

get_record() {
    local bs="$1"
    echo -n "$1> " >&2
    read dh h l p
    [ `echo $dh | wc -c` -eq  4 ] && dh='0'$dh
    return "$bs$dh,$h,$l,$p"
}

res="$(get_record "2509")"
echo $res
fu$ ./nbld.sh
2509> 3005 125 66 60
./nbld.sh[11]: 25093005,125,66,60: bad number

エェ? returnって文字列を返却できないの? マニュアルを見ると、数値(0 - 255) のみが 返却できるみたいだ。今迄、関数を作った事なんて無かったからなあ。 それから、関数中でプロンプトの表示は、stderrにする事。普通にechoしちゃうと readがfgetsを使っているせいで、入力が完了しないと表示されない。これは河豚板 のコードを眺めていて知った。

#!/bin/sh

get_record() {
    echo -n "$1> " >&2
    read dh hi lo pu dummy  
    [ "$dh" = "fin" ]         && return 0
    [ "$((dh))" -lt   100 ]   && return 255
    [ "$((dh))" -gt  3123 ]   && return 255
    [ "$((dh%100))" -gt  23 ] && return 255
    
    [ "$(echo "$dh" | wc -c)" -eq 4 ] && dh="0$dh" # adj 4 char

    [ "$((hi))" -lt 80 ] && return 255
    [ "$((lo))" -lt 50 ] && return 255
    [ "$((pu))" -lt 40 ] && return 255

    rec="$1$dh,$hi,$lo,$pu"             # real return data
    return 1    # ok
}

while true; do
    get_record "2509"
    sts=$?
    case $sts in
        0) break ;;                    # fin
        1) echo "$rec" >> data.csv ;;  # ok, append to file
        255) echo "bad data" >&2 ;;    # missing
    esac
done

readの所にdummyを設けた。これが無いと、5データ以上入力すると、puに複数 データが入ってしまう。そこで、ダミー変数に逃してあげる事にした。 これもreadの解説を真面目に読んで知った事だ。 災難?が振りかかってこないと他人事としてか頭にはいらないね。

これで、面倒そうな部分は終了かな。勿論、日時のチェックには穴が有るし、 測定値の範囲チェックを真面目にやっていないのは承知です。 例えば、最高血圧を116と入力しようとして、16と入力しちゃったような場合だ。 こういうお前はもう死にかかっているってバイタルサインは検出。でも216みたいな 日本高血圧学会が目を剥いて怒るようなのは素通りしちゃう。

長くなってきたので、続きは次回にでも。

README

ファーブルの言葉 なんて本を読んだ。

名言が収録されているんだけど、時には豆知識っぽいものの。

フランスに馬鈴薯が入った時、だれも芋を食さなかった。国王は 馬鈴薯の花がいたく気に入り、それで花束を作った。それを貴族らがが まねした。芋は相変わらず、見向きもされない。

そこ傾向をにぎにぎしく思ったパルマンチューと言う人が奇策を考えた。 馬鈴薯に触れた者は厳罰に処すと公布の上、畑を厳重な柵で保護。 護衛も付ける物ものしさ。だけど、夜はそれらの警備を解除。

近くの悪は、どんな物かとこっそり盗み出して食した。旨い。これが 密かに広まって、やがて人気を得た。こういう手もあるんだなあ。

もちろん、これだけじゃなくて、名言があふれているぞ。


This year's Index

Home