それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

curl って telnet 出来たのか。

curl って telnet 出来たのか。

curl telnet://www.google.com:80

GET / HTTP/1.1
Host: www.google.com

telnet コマンドをインストールしなくても、curlさえあればtelnetができる。

最近はtelnetコマンドをインストールしなくちゃいけないのだが、インストールしなくても使えうのが良い。

https の443 ポートにも接続できる。(できるがTLSがうまくいかないことが多い。)

curl -v  telnet://mixi.jp:443
openssl s_client -connect mixi.jp:443

TLSがあるのなら、openssl s_client を使う方がいい。

参考資料

curl で telnet を代替する - 理系学生日記

lxd のストレージ・プールbtrfsに透過圧縮(zstd)を有効にする。

btrfs で透過圧縮ができる。

btrfs はCowで透過圧縮をサポートしている。書き込み量が少なければSSDの寿命にも優しいだろう。

lxd のストレージにbtrfs を採用したのだが、透過圧縮はオフになっている。どうせなら圧縮しておこう。

現在の設定の確認

現在の設定を確認する。ストレージ一覧をだして、設定を確認

lxc storage list
lxc storage show default
lxc exec my-host --  mount  | grep btrfs

設定を確認したところ、圧縮はされてなかったのでデフォルトでは圧縮されないようです。

透過圧縮を有効にする

lxc storage set default btrfs.mount_options compress=zstd

圧縮するにはアルゴリズムを選べる。zlib, lzo, zstd から選べる。zstdでは互換性を失うが処理時間が速いので使い勝手が損なわれないようだ。圧縮率を優先するならlzo がいいのかな

lzo を使う場合

lxc storage set default btrfs.mount_options compress=lzo

設定が保存されたか確認する。

lxc storage get default btrfs.mount_options

lxd を再起動する。再起動したら反映された。

snap restart lxd

マウントオプションが変わったか確認する。

lxc exec docker --  mount  | grep btrfs

変わっていた。

/dev/vg/lxd on / type btrfs (rw,relatime,compress=zstd:3,ssd,space_cache,subvolid=259,subvol=/containers/my-host)

これでしばらく使ってみよう。

tar から名前を指定して取り出す。

tar から指定ファイルの取り出し

tar xvf archive.tar home/takuya/.config

名前の指定

tar xvf archive.tar なまえ

フォルダを指定すると、カレントディレクトリにフォルダが作成されて取り出される。

名前の取り出し

名前の一覧を見るには、

tar tv archive.tar 
lsar archive.tar 

コマンドで取り出せる。

p7zip やunarコマンドでも同じことができる。

使ってみて気づいたポイント

大きいTARファイルでは時間はかかる。

tar は、すべてをスキャンしないとファイルの存在が不明なので全ファイルをスキャンする。そういう点から20GBのtarを展開するのがめんどくさくて指定したファイルだけを取り出そうとしたが、スキャン時間はそれなりにかかった。書き込み時間は省略できるので全部展開するよりずいぶん早い。

そのため、少し時間がかかる。

高速化する方法は「SSD上で作業を行うこと」くらいだろうか。

mirakurun の b25テストが古いのかもしれないので、最新版のビルドに変えた。

mirakurun(docker)に同梱されている arib-b25-stream-test が古い

どれくらい古いのかというと、6年前にnpm に出されたままだ。

しかもnpmのくせに、C言語をmake install するだけのパッケージである。npmにあがってるので、TSを期待して探してたら完全にmake install だけでした。

しかたないので、dockerをビルドして使うことにした

mirakurun のdocker-compose.ymlの先頭を見ると、次のようになっていて、ビルドがしやすくなっている。

cat docker-compose.yml
version: "3.7"
services:
  mirakurun:
    build:
      context: ./
      dockerfile: docker/Dockerfile
    image: chinachu/mirakurun:latest
    container_name: mirakurun
mkdir docker
touch docker/Dockerfile
touch docker/install.sh

サクッと dockerfileを書いて

FROM chinachu/mirakurun:latest

COPY docker/pcsc.tgz  /app
COPY docker/pcsc-install.sh /app
##
RUN apt-get update && apt-get -y install git cmake pkg-config libpcsclite-dev
RUN /app/pcsc-install.sh

ビルド時に必要なライブラリを追加する

#!/usr/bin/env bash


tar zxvf pcsc.tgz
cd pcsc
make

cp libpcsclite.so.1.0.0 /usr/lib/arm-linux-gnueabihf/libpcsckai.so
sed '/Libs:/s|lpcsclite|lpcsckai|' \
   /usr/lib/arm-linux-gnueabihf/pkgconfig/libpcsclite.pc  \
   >/usr/lib/arm-linux-gnueabihf/pkgconfig/libpcsckai.pc


cd /opt/node_modules/arib-b25-stream-test/src
sed -i 's|libpcsclite|libpcsckai|'  Makefile
make all
make install

cd /app
git clone --depth 1  https://github.com/tsukumijima/libaribb25.git
cd libaribb25
mkdir build
cd build
cmake -DWITH_PCSC_PACKAGE=NO -DWITH_PCSC_LIBRARY=pcsckai ..
make install
cp /usr/local/bin/arib-b25-stream-test /opt/bin/arib-b25-stream-test
cd /app


echo end

これで、docker版のmirakurun でも、b25やrecdvb関連のコマンドで動作チェックが可能になった。ずいぶん昔に買って放置しているUSBのチューナーや自作のアンテナケーブルが多くあり、ちゃんと動くか片っ端からテストしたいので手軽にDocker環境が作れるのは嬉しい。

chinachuまるごと入れても良かったのだが、chinachu のpm2エラーログが溢れまくってraspiのSDカードもUSBメモリも破壊されたので、使いたくありません。

puppeteer でキャッシュフォルダのエラー

puppeteer で自動実行してたら、ずっとエラーになるのでおかしいなと思って調べた。

自分の環境では動くけどサーバーで自動実行したら動かない。

 throw new Error(`Could not find Chromium (rev. ${this.puppeteer.browserRevision}). This can occur if either\n` +
                          ^

Error: Could not find Chromium (rev. 1083080). This can occur if either
 1. you did not perform an installation before running the script (e.g. `npm install`) or
 2. your cache path is incorrectly configured (which is: /root/.cache/puppeteer).

理由は、キャッシュフォルダだった。

/root/.cache/puppeteer

キャッシュフォルダは、uid から拾わずに、env から拾っている。そういえば、sudo して実行してたわ。

対応策

export HOME=/home/takuya
sudo -u takuya node bin/my-puppeteer.js

rclone でSFTPに接続できない。

rclone で sftpに接続しようとしたらエラーになる。

NewFs: couldn't connect SSH: ssh: handshake failed: ssh: unable to authenticate,

原因がわからずパニックになるが、sshはRSA鍵をデフォルトで無効化したことを思い出した。多分これだろうと予想を立てて対応

sshd のログを見てみる

Mar 19 22:22:05 docker sshd[507223]: userauth_pubkey: key type ssh-rsa not in PubkeyAcceptedAlgorithms [preauth]

サーバー側に設定を追加

/etc/ssh/sshd_config

PubkeyAcceptedAlgorithms +ssh-rsa
sudo service sshd reload

つながった。

考えられる原因。

rclone がSSHプロトコルRSAを使っている。rcloneはSFTPをRSAで行おうとしている。rcloneが古いのかもしれない。最新版のビルドでは対応済みっぽいですね。

sshdの仕様変更が思わぬ波及効果を生み出している。

致命的な問題が出ない限りRSA鍵の許可を追加でも問題は起きないと思う。

apt-cacher をnginxの後ろに置こうとしてハマる

apt-cacher を443 でnginxの後ろに置こうとした。

動かない。

ログを見た。

10.10.1.240 - - [17/Mar/2023:17:45:20 +0900] "CONNECT esm.ubuntu.com:443 HTTP/1.1" 400 166 "-" "-"
10.10.1.240 - - [17/Mar/2023:17:45:20 +0900] "CONNECT esm.ubuntu.com:443 HTTP/1.1" 400 166 "-" "-"
10.10.1.240 - - [17/Mar/2023:17:45:20 +0900] "CONNECT download.docker.com:443 HTTP/1.1" 400 166 "-" "-"
10.10.1.240 - - [17/Mar/2023:17:45:21 +0900] "CONNECT esm.ubuntu.com:443 HTTP/1.1" 400 166 "-" "-"
10.10.1.240 - - [17/Mar/2023:17:45:21 +0900] "CONNECT esm.ubuntu.com:443 HTTP/1.1" 400 166 "-" "-"
10.10.1.240 - - [17/Mar/2023:17:45:22 +0900] "CONNECT download.docker.com:443 HTTP/1.1" 400 166 "-" "-"
10.10.1.240 - - [17/Mar/2023:17:45:23 +0900] "CONNECT esm.ubuntu.com:443 HTTP/1.1" 400 166 "-" "-"
10.10.1.240 - - [17/Mar/2023:17:45:23 +0900] "CONNECT esm.ubuntu.com:443 HTTP/1.1" 400 166 "-" "-"
10.10.1.240 - - [17/Mar/2023:17:45:26 +0900] "CONNECT download.docker.com:443 HTTP/1.1" 400 166 "-" "-"
10.10.1.240 - - [17/Mar/2023:17:45:27 +0900] "CONNECT esm.ubuntu.com:443 HTTP/1.1" 400 166 "-" "-"
10.10.1.240 - - [17/Mar/2023:17:45:27 +0900] "CONNECT esm.ubuntu.com:443 HTTP/1.1" 400 166 "-" "-"

CONNECTじゃん。nginxでは対応できない。

そうだよね。冷静に考えたらリバプロではく、フォワード・プロキシになるので、CONNECTが必要だよね。CONNECTなど素のnginxで動くはずもないので諦める方がいい。

nginx で Connect できなくても stream で流せば良いんでしょ?

nginxをプロキシで使ってるとついつい次のような発想になりがち

nginxでTCPさばきました

stream で流すって単なるNAT(SNAT+DNAT)じゃん?

それ、iptablesのDNATでやったほうが良いよ。

nginxだと、SNATと同等になるので接続ログの管理がめんどくさいいと思います。

素のTCPしかしゃべらないプロトコルを強引にSSL化するSSLアクセラレータとしてTCPをSTREMする場合は、次のようになるので、この場合はiptablesよりnginxに理由があるのですが、streamでTCPを捌くだけの記事を読んでいてiptablesがめんどくさいんだろうなわかる、とおもいつつもnginxでやる意味・・・とモヤッとした。

stream{
  upstream apt-cacher {
    server my_service:8000;
  }
  map $target $name{
    example.tld apt-cacher
    default 127.0.0.1:443
  }

  server {
    listen  127.0.0.2:443 ssl;
    proxy_pass $name;
    ssl_preread on;
    ssl_certificate     /var//certs/nginx/cert.pem;
    ssl_certificate_key /var//certs/nginx/key.pem;

    proxy_connect_timeout 1s;
    proxy_timeout 3s;
  }
}

上記のように平文通信の間にnginxを挟み込んで、TLS化するのはありだと思った。

stream mapを使ってALPNホスト名ごとに接続先を変えるのは便利だと思う。

apt-cacher で試したが、httpsのaptではうまく動かなかった。

参考資料

https://stackoverflow.com/questions/34741571/nginx-tcp-forwarding-based-on-hostname

ubuntuのaptで入れたsslhで細かい設定を行う。

ubuntu の apt で入れた sslh が微妙だった件。

ubuntu の apt で入れた sslh にいくつか問題があった。どのようにめんどくさいかというと、設定の追加がほぼ不可能なのだ。

/etc/init.d/sslh/ をベースにしていて、 init.dが/etc/default/sslhsourceして起動するが、init.d は systemd 管理下に置かれたので、 sourceをせずにsystemdに変数を設定として文字列を拾われる。

このため/etc/default/sslh/bin/shを使うsource前提にならず、systemd経由の起動されるため変数展開が反映されない。このため起動設定をbin/shで記述ができなくなってる

さらに、sslhは forking で起動するのに、foreground が前提でtype simple に固定されている。そのためpkillで一時的に止めることもかなわない。ユニットの編集は必須である。

さらに、inetd を基本線にしているのか、設定をsystemdに書くのか、init.dでやるのかdefaultsに書くのかと判断に迷う。恐ろしく設定がめんどくさい。

apt で導入される systemdユニットを捨てて書き直すか、自分でカスタマイズするしかない。

systemd ユニットを上書きする

 sudo systemctl edit sslh.service

次のように上書きした。

# /etc/systemd/system/sslh.service.d/override.conf
## 2023-03-17 /etc/defaultsに変数が使えないので代替案
[Service]
ExecStartPre=/usr/bin/bash /etc/sslh/sslh-start-pre.sh
ExecStartPre=/usr/bin/bash /etc/sslh/transparent-iptables.sh
KillMode=control-group
PIDFile=/run/sslh/sslh.pid
KillMode=control-group
KillSignal=SIGTERM
Type=simple

起動前にシェルスクリプトを実行して /etc/default/sslhを動的に書き換えることにした。

/etc/sslh/sslh-start-pre.sh

シェルスクリプトで動的に書き換えるために、テンプレートを使い変数を展開し、/etc/defaults を書き換えるようにした。

#!/usr/bin/env bash
#DAEMON_OPTS="--user sslh --listen <change-me>:443 --ssh 127.0.0.1:22 --ssl 127.0.0.1:443 --pidfile /var/run/sslh/sslh.pid"


ADDR=$( ip -o  addr show | grep eth | grep -oP '192.168.[0-9]{1,3}.[0-9]{1,3}(?=/24)' )
OPTS="--user sslh --listen $ADDR:443 --ssh 127.0.0.1:22 --tls 127.0.0.1:443 --pidfile /run/sslh/sslh.pid"
OPTS="-F/etc/sslh/sslh.conf --listen $ADDR:443"


DAEMON_OPTS=$OPTS

echo "# Default options for sslh initscript
# sourced by /etc/init.d/sslh

# binary to use: forked (sslh) or single-thread (sslh-select) version
# systemd users: don't forget to modify /lib/systemd/system/sslh.service
DAEMON=/usr/sbin/sslh


# auto generate by takuya
DAEMON_OPTS=$OPTS


" > /etc/default/sslh

リッスンするIPアドレスとポートをDHCPの配布状況に応じて書き換える。

/etc/sslhに設置した設定ファイルを使うようにする。

透過プロキシ用iptables

#!/usr/bin/env bash
## sslhでtransparent を使うには、必要な設定
##

IFACE=$( ip  addr show | grep eth | grep -P '192.168.2.[0-9]{1,3}(?=/24)' | grep -oP 'eth[0-9]' )

function add(){
  sysctl -w net.ipv4.conf.default.route_localnet=1
  sysctl -w net.ipv4.conf.all.route_localnet=1

  # DROP martian packets as they would have been if route_localnet was zero
  # Note: packets not leaving the server aren't affected by this, thus sslh will still work
  iptables -t raw -A PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP
  iptables -t mangle -A POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP

  # Mark all connections made by ssl for special treatment (here sslh is run as user "sslh")
  iptables -t nat -A OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f

  # Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark)
  iptables -t mangle -A OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f

  # Configure routing for those marked packets
  ip rule add fwmark 0x1 lookup 100
  ip route add local 0.0.0.0/0 dev lo table 100
}

function down(){

  iptables -t raw -D PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP
  iptables -t mangle -D POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP
  iptables -t nat -D OUTPUT -m owner --uid-owner sslh -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x01/0x0f
  iptables -t mangle -D OUTPUT ! -o lo -p tcp -m connmark --mark 0x01/0x0f -j CONNMARK --restore-mark --mask 0x0f
  ip rule del fwmark 0x1 lookup 100
  ip route del local 0.0.0.0/0 dev lo table 100

}

function main(){
  case $1 in
    add)
      add;
      ;;
    del)
      down;
      ;;
  esac

}


main $@

/etc/sslh/sslh.conf

リッスンするアドレスとポートは、systemdから起動時のオプションで与えるため設定では省略

systemdはforegroundは常にTrueを想定するのでtrueにした。

foreground: true;
inetd: false;
numeric:false;
transparent: true;
timeout: 2;
user: "sslh";
pidfile: "/var/run/sslh.pid";
chroot: "/etc/sslh";


listen:()

protocols:
(
  { name: "ssh"; service: "ssh"; host: "localhost"; port: "22";
    keepalive: true; fork: true; tfo_ok: true },
  { name: "tls"; host: "localhost"; port: "443";  tfo_ok: true }
);

つながった

curl で動作確認して、疎通を確認した。

これで無事にsslhを使って接続を仕分けられる。

sslhのメリット/ nginxの設定がIP非依存に

現在では、nginxの前段にsslhを入れている。

nginxの設定では、次のように、listenを記述している。

listen 127.0.0.1:443 ssl http2;

このように書けるので、nginxの設定ファイルを使い回せるようになった。環境依存の固定IPアドレスがnginxの設定ファイルから消えて、nginxの設定が環境非依存になりコピペがしやすくクラスタリングも楽になった。CD/CIファイルの記述も減った。

初期接続開始のTCPセッション・TLSセッション時間が極僅かだけ遅くなったが許容範囲だった。

プロセス管理でプロセスグループをkillする

プロセスグループをKILLしたい。

プロセスからfork したプロセスから forkしたプロセス、子プロセス・孫プロセスを除去したい。

プロセスグループの確認

ps -ejH で PGIDとSIDを確認する

ps -ejH | 
  PID  PGID   SID TTY          TIME CMD
19666 19666 19666 ?        00:00:00   ssh
19668 19666 19666 ?        00:00:00     ssh

まともなプログラムを書いているなら、ForkしたときにPGIDを設定して子プロセスとして生成しているはず。

プロセスの親子関係や、プロセスグループ、プロセスの祖先と子孫がたどれるようにプロセスは生成される(ようにプログラムを書いているはず、もし辿れないなら意図的に別プロセスで起動しようとして失敗しているかもしれない。)

プロセスをkillする

親のプロセスIDがわかるなら、pkillを使うのが早い

pkill -P 親のPID

killコマンドで負数を指定する(pkillがない場合など)

kill -- -1234

プロセスを識別せずに全部消したい

commandの文字列にマッチさせてすべて除去したいのであれば killall が使える。

killall でコマンドを全部消す。

killall /usr/bin/ssh

複数起動していて残すべきものがない場合は、すべて消せる。そのときは killall で手っ取り早い。

また、pkillでもできる。

pkill /usr/bin/ssh

プロセスを消す場合には起動時に覚えておく。

pidファイルを作って保存しておけば、PIDファイルを見てグループを除去できる。

普段使わないから、なかなか覚えられないんですよねぇ。

参考資料

openWrtのFirewallにホットプラグする。

OpenWrtはホットプラグで「〇〇したとき」に任意のスクリプトを実行できる。

interface(iface)で、接続時に〇〇するとかできる。

Firewallが追加されたときに、なにかしたいと思ったが、公式サイトに記述がない。

イベントを調べた。

mkdir /etc/hotplug.d/firewall
touch /etc/hotplug.d/firewall/sample.sh
chmod +x /etc/hotplug.d/firewall/sample.sh

/etc/hotplug.d/firewall/sample.sh

## 実行時に変数を保存して調べる。
echo '-----------' > /tmp/firewal.$ZONE
env >> /tmp/firewal.$ZONE

環境変数でファイルを調べる。 Firewallの追加はゾーン単位で起動され非同期(ノン・シーケンシャル / 並列)で行われる。

イベントの識別。

変数は次のようになっていて、必要なイベントを拾うことができる。

環境変数
ZONE FWゾーン wan
USER 実行ユーザ root
ACTION 手順種別 add
SHLVL シェルの深さ 1
HOTPLUG_TYPE 種別 firewall
LOGNAME ログ名 root
DEVICENAME バイス名? 空白
PATH 実行PATH /usr/sbin:/usr/bin:/sbin:/bin
INTERFACE インターフェース vpn_net
PWD 実行DIR /
DEVICE バイス eth1

サンプル

case と if を使って必要なイベント時にだけ実行されるようにする。

#!/bin/bash


function my_firewall(){
    case $1  in
      add)
        my_iptables add
        ;;
      remove)
        my_iptables add
        ;;
    esac
}


TARGET_ZONE=wan
TARGET_NAME=pppoe_isp
TARGET_HOTPLUG=fiewall


function main(){
  if [[ ! $HOTPLUG_TYPE == $TARGET_HOTPLUG ]]; then
    return 0;
  fi
  if [[ $ZONE == $TARGET_ZONE && $DEVICE==$TARGET_NAME  ]];then
    my_firewall $ACTION
  fi
}

main;

ホットプラグで便利

ホットプラグでファイアウォール使うと、スクリプトを使ってiptablesを使いやすい。LuCIに書くと複雑になりがちな条件をまとめて書くことができて便利だ。

参考資料

https://openwrt.org/docs/guide-user/base-system/hotplug

curlが404でif文を判定する。exit code を httpステータスに従わせる。

curlは 接続に成功したら exit code == 0 である。

接続に成功したら、404・500でも 結果は 0 になる。

curl -s  http://example.tld ; echo $?

httpステータスコードを検証する

接続に成功し、HTTP取得に成功したときだけ何かをしたい。

curl --fail -s  http://example.tld && echo Success

稼働チェックをするときに重要です。

ファイルを空にするlinuxコマンド

ファイルを空っぽにしたい。

dhcp leases とか log とかファイルを空っぽにしたい事があると思います。

rm && touch

単純に考えると、rm して touch ですが。パーミッションが初期化されるので面倒です。

rm fileA && touch fileA

空文字を書き出す。(リダイレクト)

/dev/null から空っぽを取り出せばいいのです。

cat /dev/null  > /tmp/dhcp.leases

この他にも、リダイレクトを応用すると次のような方法があります。

: > file
echo -n > file
print '' > file

bash の コロン(:)は 何もしない(true)です。なので、: > a は次のようにもかけます。
また falseも結果としておなじになるのでファイルを空っぽにすることができます。

: > file 
true > file
false> file

ファイルを空にするにはリダイレクトが便利なようです。

空っぽへ編集する。

sed を用いて、空ファイルへ編集すればいいとわかります。

sed 'd' -i a

sed で全行(指定省略)にたいして、d (delete)を発行します。

同等のことはvi/ex/ed コマンドでもできるかもしれませんが試してません。

vim なら ex モードを使って書くことも可能ですが、現実的ではありませんでした。

vim -e +'norm dG' +'wq'  fileA

空っぽのファイルをコピーする

/dev/null はいつでも空っぽなので cp コマンドでもできます。

cp /dev/null file

dd する

/dev/null はいつでも空っぽなので dd コマンドでもできます。

dd if=/dev/null of=/tmp/echo bs=1 count=0

truncate コマンド

この目的(ファイルを指定サイズにする)ためには、truncate コマンドが便利です。

truncate -s0 file 

ただし、busyboxなどの極限環境ではtruncateは使えないことがあると思います。

alias を使う

truncate がない場合などに、aliasや関数を使うと便利かもしれない。

function trancate(){
  dd if=/dev/null of=$1 bs=$2 count=1
}

参考資料

vimをパイプにする - 余白の書きなぐり

Let'Encrypt発行証明書を使って中間CAを作るとどうなるのか。

Let'Encrypt発行の証明書を使って中間CAを作るとどうなるのか。

Let'Encrypt発行の証明書は、次の用途に使える。

openssl x509 -purpose -noout   < cert.pem
Certificate purposes:
SSL client : Yes
SSL client CA : No
SSL server : Yes
SSL server CA : No
Netscape SSL server : Yes
Netscape SSL server CA : No
S/MIME signing : No
S/MIME signing CA : No
S/MIME encryption : No
S/MIME encryption CA : No
CRL signing : No
CRL signing CA : No
Any Purpose : Yes
Any Purpose CA : Yes
OCSP helper : Yes
OCSP helper CA : No
Time Stamp signing : No
Time Stamp signing CA : No

openssl x509 -purpose で見る限りxxx CA no が並んでいる。Any Purpose CA : Yes がYESなので、ワンちゃん使えるかも?みたいな気持ちになって、試してみようと思う。*1

認証局を作る。

前回に中間CAを作ったりして証明書を作る手順は理解できたので、Let'Encryptの鍵と証明書で試してみることができる。

認証局ディレクトリを整える。

mkdir MyCA
cd MyCA
mkdir ./newcerts
mkdir ./requests
touch index.txt
echo 00 > serial
echo 00 > crlnumber

認証局用の設定を作る

cat /etc/ssl/openssl.cnf \
 | sed -n '/\[ ca \]/,$p' \
 | sed  '/\[ tsa/,$d' \
 | sed -e '/^keyUsage = nonRepudiation/s|^|# |' \
 | sed -e '/keyUsage = cRLSign/s|^# ||' \
 | sed -e 's|365|3600|'  \
 | sed -e 's|\./demoCA|\.|'  \
 > myCA.openssl.cnf

Let'Encryptから鍵と証明書を取り出す。

sudo cp /etc/letsencrypt/live/${DOMAIN}/privkey.pem ./
sudo cp /etc/letsencrypt/live/${DOMAIN}/cert.pem ./
sudo cp /etc/letsencrypt/live/${DOMAIN}/chain.pem ./
sudo chown takuya:takuya privkey.pem
sudo chown takuya:takuya cert.pem
sudo chown takuya:takuya chain.pem

証明書を発行する

任意の公開鍵を作り署名リクエストを作る。

openssl genrsa > server.priv.key
openssl req -new \
  -subj "/C=JP/ST=Kyoto/L=Kyoto City/O=acid/CN=raspi3.lan" \
  -key server.priv.key \
  > server.csr

Let'Encryptの秘密鍵で任意の証明書に署名する。

openssl ca -policy policy_anything \
   -in server.csr \
   -keyfile privkey.pem \
   -cert  cert.pem \
   -config myCA.openssl.cnf

署名して出来た証明書を取り出す。

openssl x509 -text < newcerts/00.pem
openssl x509 < newcerts/00.pem > server.crt.pem

検証のために証明書チェーンを作る。

cat cert.pem chain.pem  > full.pem

検証する。

openssl verify -verbose -CAfile full.pem server.crt.pem

検証結果。

CN = xxxx.mydomain.com
error 24 at 1 depth lookup: invalid CA certificate
C = US, O = Let's Encrypt, CN = R3
error 25 at 2 depth lookup: path length constraint exceeded
O = Digital Signature Trust Co., CN = DST Root CA X3
error 10 at 4 depth lookup: certificate has expired
CN = xxxx.mydomain.com
error 32 at 1 depth lookup: key usage does not include certificate signing
error server.crt.pem: verification failed

こうなる。

key usage does not include certificate signing

利用不可の証明書が出来た

予想通りの結果でした。証明書を作成する手前でRejectされるかと思ったが、証明書の発行は出来ている。証明書を通用させようとして検証するとエラーになる。なるほどね。

使えないとわかった。

証明書自体は作れるが、証明書を利用時に検証されて、エラーになる。

SAN値ルーターのIPとか入れてローカルアドレスHTTPS化できたら嬉しかったんだけど。無理なようです。残念。

参考資料

https://serverfault.com/questions/111625/openssl-x509-purpose-flag-any-purpose-what-is-this

The Any Purpose : Yes and Any Purpose CA : Yes lines from the openssl x509 -purpose are special. It is openssl specific and represents what the certificate will be validated for when used with ancient software versions that do not check for extensions.

certificate — OpenSSL x509目的フラグ「任意の目的」これは何ですか?

'「任意の目的」設定は、何でも通過させ、チェックをまったく実行しないものです。もともとは、他に選択肢がなく、結果に耐えられる場合に壊れた証明書を使用する方法としてそこに置かれていました...それ以来、「任意の目的」が設定されている場合でも、コードでCAチェックが必須になっています。したがって、実際にその証明書をCAとして使用しようとすると、拒否されます。

*1:といってもAny Purposeは歴史的経緯で残ってるだけで使えるものではないのだけれど

TLS上で通信するプロコル選択ALPNとは

ALPNとはなんなのか

ALPN=Application-Layer Protocol Negotiation

アプリケーション側からのプロトコル要求です。

nginx では、これをサポートする

HTTPサーバーがALPNで応答しているか確認する

 echo | openssl s_client -alpn h2 -connect 192.168.1.1:443 2>/dev/null | grep h2

ALPNは「アプリケーション」側からの提案であって、サーバー側は受容するだけですね。昔はいろいろあったそうですが。

参考資料を見て学びました。

参考資料

https://christina04.hatenablog.com/entry/2016/07/30/122027

OpenWrtでSSLHをしてHTTPS/TLSを443共有する

OpenWrtでSSLHをしてHTTPS/TLSを443共有する

TCP443ポートをSSLHで再利用する。OpenWrtで行う。

インストール

opkg install sslh
uci show  sslh

TLS(https)/SSHLの転送先

uci set sslh.default.ssh='10.1.1.1:22'
uci set sslh.default.tls='10.1.1.1:443'

uci set sslh.default.listen='xxx.xxx.xxx.xxx:443' ## ここ注意

uci set sslh.default.enable='1'
uci set sslh.default.transparent='1'

ここはPPPOEが変わるたび設定を変える必要がある。

LuCi(WEB管理画面)との衝突を回避

LucI がHTTPSを使う場合、0.0.0.0 とバッティングするので注意する。

luci のHTTPS0.0.0.0/0から指定ポートに振り変える

uci show uhttpd.main
uci add_list uhttpd.main.listen_https='$LAN_IP:443'
uci add_list uhttpd.main.listen_https='$VPN_IP:443'
uci get uhttpd.main.listen_https

再起動で反映。

service uhttpd restart

少々ややこしいが、SSHLの安定後なら、SSHLを使ってluci の管理画面自体もsslhで振り分けても良い。(ポートフォワーディング+APLN+TLSで可能)

443(tcp)への通信を許可

OpenWrtへの443ポート通信をAcceptする。

uci add firewall rurle
uci add_list firewall.@redirect[-1]='rule'
uci set firewall.@rule[-1].name='allow-443-sslh'
uci set firewall.@rule[-1].proto='tcp'
uci set firewall.@rule[-1].src='wan'
uci set firewall.@rule[-1].dest_port='443'
uci set firewall.@rule[-1].target='ACCEPT'

設定を確認して反映

uci show firewall
## 問題がなさそうなら
uci commit firewall

ファイアウォールの設定の代わりに、ポートフォワーディングでsslhへ転送しても良い。

透過(transparent)設定

このままでは、sslh がプロキシしているので、HTTPSSSHのログにルーターのIPが残るだけである。SNATしてるのと変わらない。 流石にこれはセキュリティ設定やログ管理で不便なので、透過にする。

透過設定は少々ややこしいが、SSLH公式にサンプルがある

透過にするには、iptablesでmarkしてルーティングテーブルを変えてあげなきゃいけない。

transparent モジュール(kmod-ipt-tproxy)を入れる

opkg install iptables-mod-tproxy 

有効にする。

iptables -t mangle -N SSLH
iptables -t mangle -A PREROUTING -p tcp -m socket --transparent -j SSLH
iptables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 -m multiport --sport 443 --jump SSLH
iptables -t mangle -A SSLH --jump MARK --set-mark 0x1
iptables -t mangle -A SSLH --jump ACCEPT
ip rule add fwmark 0x1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100

接続を実験する

ssh -t my-cloud-vps.tld 'nc my-router 443'

接続ができたら問題なさそう。

sslh永続化する。

sslh のリッスンポートは毎回IPを指定しなくちゃいけないので設定を変える。

/etc/hotplug.d/iface/98-ifup-pppoe.sh

#!/usr/bin/env sh

## PPPoE接続後に、 スクリプトを実行する
## 2023-03-14
function get_current_ip(){
  . /lib/functions/network.sh
  network_flush_cache
  network_find_wan WAN_IF
  network_get_ipaddr IP_ADDR "${WAN_IF}"
  echo $IP_ADDR
}

function restart_sslh(){
  echo restart sslh
  source /etc/config/custom/change_ip.sh
  uci set sslh.default.listen=`get_current_ip`:443
  uci commit;
  /etc/init.d/sslh restart
  sslh_transparent;
}

function sslh_transparent(){

   ## down
  iptables -t mangle -D PREROUTING -p tcp -m socket --transparent -j SSLH
  iptables -t mangle -D OUTPUT --protocol tcp --out-interface eth0 -m multiport --sport 443 --jump SSLH
  iptables -t mangle -D SSLH --jump MARK --set-mark 0x1
  iptables -t mangle -D SSLH --jump ACCEPT
  iptables -t mangle -X SSLH
  ip rule del fwmark 0x1 lookup 100
  ip route del local 0.0.0.0/0 dev lo table 100
   ## up
  iptables -t mangle -N SSLH
  iptables -t mangle -A PREROUTING -p tcp -m socket --transparent -j SSLH
  iptables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 -m multiport --sport 443 --jump SSLH
  iptables -t mangle -A SSLH --jump MARK --set-mark 0x1
  iptables -t mangle -A SSLH --jump ACCEPT
  ip rule add fwmark 0x1 lookup 100
  ip route add local 0.0.0.0/0 dev lo table 100

}

## main
function main(){
  TARGET_INTERFACE=myISP
  TARGET_DEVICE=eth1
  [ "$ACTION" = "ifup" -a "$INTERFACE" = "$TARGET_INTERFACE"  ] && {
    logger "iface $TARGET_INTERFACE / $TARGET_DEVICE up detected, do hotplug actions."
    sleep 1;
    logger "start hotplug actions."
    ## ホットプラグの/bin/sh実行シェルに注意
    restart_sslh &
    ##
    logger "hotplug actions is done.:";
  }
}


main

このような設定を仕込むが面倒なので。uhttp(LuCI)の管理画面の443を何処か別ポートに振り分けて、SSLHで捌いたほうが良さそう

SNIを入れたい

opkg で導入されている、sslh の起動スクリプト( /etc/init.d/sslh )がSNIなどに未対応のため、細かい設定が不可能だった。

もし、/etc/sslh.confなどを使って設定を書きたいなら、自分で起動スクリプトを作成する。

不十分なopkg添付の起動スクリプトを使うより、ちゃんと起動スクリプトを書いたほうが、あとあと楽でいい。

さっきホットプラグで仕込んだ transparent の iptables / fwmark も起動スクリプトに合体させられる。設定を管理しやすい 。

自作の起動スクリプトに切り替える。

既存の起動スクリプトを放棄する。

/etc/init.d/sslh disable
mv /etc/init.d/sslh /etc/init.d/sslh.opkg

自作の起動スクリプトに切り替える

mkdir -p /etc/config/custom/sslh
cd /etc/config/custom/sslh

必要なファイルを作る。

touch sslh-daemon.sh
touch sslh.conf
touch transparent-sslh.sh

sslh-daemon.sh

#!/bin/sh /etc/rc.common
START=95
STOP=01

function get_current_ip(){
  . /lib/functions/network.sh
  network_flush_cache
  network_find_wan WAN_IF
  network_get_ipaddr IP_ADDR "${WAN_IF}"
  echo $IP_ADDR
}


dir=/etc/config/custom/sslh
conf=$dir/sslh.conf
pidfile=`cat $conf | /bin/grep -o -E  'pidfile.+' | grep -Eo '".+"' | sed 's|"||g'`
transparent_sslh=$dir/transparent-sslh.sh
sslh=`which sslh`
current_ip=`get_current_ip`
if [ -z $sslh ]; then
  logger 'sslh daemon failed. sslh command not found.'
  exit
fi


start(){
  echo start sslh
  echo $sslh -F $conf --listen $current_ip:443
  $sslh -F $conf --listen $current_ip:443
  $transparent_sslh add
  logger 'sslh daemon started.'
}

stop() {
  echo stop sslh
  if [ -e $pidfile ] ; then
    pid=`cat $pidfile`
    kill $pid
    logger 'sslh daemon stopped.'
    $transparent_sslh del
  fi
}

transparent-sslh.sh

#!/bin/sh

function sslh_transparent_add(){
  ## up
  iptables -t mangle -N SSLH
  iptables -t mangle -A PREROUTING -p tcp -m socket --transparent -j SSLH
  iptables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 -m multiport --sport 443 --jump SSLH
  iptables -t mangle -A SSLH --jump MARK --set-mark 0x1
  iptables -t mangle -A SSLH --jump ACCEPT
  ip rule add fwmark 0x1 lookup 100
  ip route add local 0.0.0.0/0 dev lo table 100

}

function sslh_transparent_del(){

   ## down
  iptables -t mangle -D PREROUTING -p tcp -m socket --transparent -j SSLH
  iptables -t mangle -D OUTPUT --protocol tcp --out-interface eth0 -m multiport --sport 443 --jump SSLH
  iptables -t mangle -D SSLH --jump MARK --set-mark 0x1
  iptables -t mangle -D SSLH --jump ACCEPT
  iptables -t mangle -X SSLH
  ip rule del fwmark 0x1 lookup 100
  ip route del local 0.0.0.0/0 dev lo table 100

}
function sslh_transparent_start(){
  sslh_transparent_del;
  sslh_transparent_add;
}

function main(){
  #  sslh_transparent_start;
  case $1 in
    "add")
      echo transparent iptables,route added.
      sslh_transparent_add;
      ;;
    "del")
      echo transparent iptables,route removed.
      sslh_transparent_del;
      ;;
    *)
      echo "usage"
      echo "     " $0 ' del'
      echo "     " $0 ' add'
  esac
}

if [[ $0 =~ 'transparent-sslh.sh' ]] ; then
  # execute する場合
  main $*
fi

sslh.conf

# ##########################
#  2023-03-14
#  takuya
# ###########################

verbose: 0;
foreground: false;
inetd: false;
numeric: false;
transparent: true;
timeout: 2;
user: "nobody";
pidfile: "/var/run/sslh.pid";
chroot: "/var/tmp";


# Listenアドレスは、設定に書かずにコマンド引数で与える
listen: ();

protocols:
(
  {
    name: "ssh";
    service: "ssh";
    host: "10.16.0.1";
    port: "22";
    fork: true;
  },
  {
    name: "tls";
    alpn_protocols: [ "h2", "http/1.1", "spdy/1", "spdy/2", "spdy/3" ];
    host: "10.16.0.1";
    port: "443";
    sni_hostnames: [
      "example.tld"
    ];
    log_level: 0;
    tfo_ok: true
  },
  {
    name: "tls";
    alpn_protocols : [ "h2", "http/1.1", "spdy/1", "spdy/2", "spdy/3" ];
    host: "10.8.0.1";
    port: "443";
    log_level: 0;
    tfo_ok: true
  },
);

接続を試す。

sslh -F /etc/config/custom/sslh/sslh.conf -f -v 5  --listen $current_ip:443 

接続ができたら、起動スクリプトをコピーして起動する

ln -s /etc/config/custom/sslh/sslh-daemon.sh /etc/init.d/sslh
/etc/init.d/sslh enable 
/etc/init.d/sslh start

L7 ルータとして

OpenWrtでsslhがHTTPS(443)のL7ルータして起動する。

これで、nginx の手前で、ホスト名を見て接続先を切り替えることができる。便利ですよね。

2023-03-15 追記

OpenWrtでsslh をtranspaernt起動していると、LANからのLAN -> PPPoEのグローバルIP -> sslh -> nginx の通信が、sslh へ戻らずに、transparent のために、直接LAN上に投げ出される。

transparent なので次の通信がおかしくなる。

LAN(192.168.1.5) -> sslh(192.168.1.1) -> nginx (192.168.1.10)

sslhが透過だと、nginx は lan のIPアドレスが見えてるので、Default GW へ戻らずに、直接 (nginx (192.168.1.10 -> 192.168.1.5)パケットを投げ返してしまう。

そこで、sslh のホスト上でSNATを引っ掛けてあげる。

## wrt側での対応例
iptables -t nat -I POSTROUTING --protocol tcp -s $LAN_IP/24 -d $HTTPS_SERVER_IP -i lo --dport 443 -j SNAT --to-source $ROUTER_IP

または、nginxホスト上でパケットが常にGWへ流れるようにする。

## または、nginx側でGWを固定する。
iptables -t mangle -N SSLHSSL
iptables -t mangle -A OUTPUT -o eth0 -p tcp -m multiport --sport 443 -j SSLHSSL
iptables -t mangle -A SSLHSSL --jump MARK --set-mark 0x1
iptables -t mangle -A SSLHSSL --jump ACCEPT
ip rule add fwmark 0x1 lookup 100
ip route add default via 192.168.1.1 table 100
ip route flush cache

透過のときは考えることが多いですね。

2023-03-15 追記2 firewall 再起動/PPPoE再起動

sslh を透過で使うと、firewall ( iptables / route ) を書き換えている。そのためLuciなどで firewall restart をすると設定が飛んでしまう。 またリッスンアドレスをPPPOEにしているので再接続時にリッスンできなくなる。

sslh 用の設定を firewall に記述して解決するが、firewall の設定がsslh に依存するのは嬉しくないので、hotplugを使って連動させる。

pppoe と連動させているだけでは足りなかった。

mkdir -p /etc/hotplug.d/firewall
touch /etc/hotplug.d/firewall/01.sslh
chmod +x /etc/hotplug.d/firewall/01.sslh

/etc/hotplug.d/firewall/01.sslh

#!/bin/bash
function sslh_firewall(){
  /etc/config/custom/sslh/transparent-sslh.sh $1
}

function main(){
  if [[ ! $HOTPLUG_TYPE == 'firewall' ]]; then
    return 0;
  fi
  if [[ $ZONE == 'wan' && $DEVICENAME='pppoe-ybb' ]];then
    case $ACTION in
      add)
        :
        echo '----add' >> /tmp/echo.$ZONE
        env >> /tmp/echo.$ZONE
        echo '----' >> /tmp/echo.$ZONE
        sslh_firewall add
        ;;
      remove)
        :
        echo '----remove' >> /tmp/echo.$ZONE
        env >> /tmp/echo.$ZONE
        echo '----' >>/tmp/echo.$ZONE
        sslh_firewall del
        ;;
    esac
  fi
}

main;

firewall の hotplugは環境変数を使って識別する。

ZONE=wan
USER=root
ACTION=add
HOTPLUG_TYPE=firewall
INTERFACE=isp
PWD=/
DEVICE=pppoe-isp

ACTION, ZONE,DEVICE で識別する。 ZONEにインターフェースが複数入ってるとZONE=lan action=addが複数回呼ばれるので注意。DEVICEまで含めて識別する。

firewall から削除されるとき、ACTION=remove で呼ばれる。firewallに追加されるときにACTION=addで呼ばれる。

INTERFACEとDEVICEが逆になってる気もするが。。。。

これで再起動後も問題なく動くはずである。