tcpdump でキャプったデータから TCP ストリームを取り出す

tcpdump で次のようにキャプって保存したファイルは wireshark で開いて解析できます。

tcpdump -nn -w a.cap port 80

wireshark なら tcpdump よりもいろいろな解析が可能なので、よくサーバでキャプったファイルから HTTP などの TCP ストリームを取り出すのに使っていました。

がしかし、「サーバ上で tcpdump でファイルに保存 → ローカルの落としてきて wireshark で開く」というのが面倒くさくて、CLI だけでできればなーと常々思っていました。

・・・すみません、常々思っていたにも関わらずまったく調べていませんでした。ググれば普通に CLI で出来るツールがでてきました。

tshark

wireshark の CLI 版です。

CentOS 7 とか 8 とか Fedora 31 とかなら標準のリポジトリから入れられます。

# CentOS 7
sudo yum -y install wireshark

# CentOS 8 / Fedora 31
sudo dnf -y install wireshark-cli

CentOS 8 や Fedora 31 で wireshark を入れると X 関係が含まれるのか大量の依存パッケージがインストールされてしまいます。tshark だけなら wireshark-cli で十分です。

次のように tcpdump でキャプチャしたファイルが読めます。-z オプションの末尾の 1 はストリーム番号です。キャプチャファイルに複数のストリームがあるときは番号を変えて別々に実行する必要があります。

tshark -n -q -z follow,tcp,ascii,1 -r a.cap

次のように直接キャプチャすると、キャプっているパケットの概要が逐次表示されつつ、Ctrl+C で止めたときに TCP ストリームが表示されます。

tshark -n -z follow,tcp,ascii,1 port 80

tcpdump からパイプするときは Ctrl+C で tshark が SIGINT で死ぬとなにも表示されません。setsid で SIGINT されないようにすれば大丈夫です。

tcpdump -nn port 80 -w - | setsid tshark -n -z follow,tcp,ascii,1 -r -

tcpflow

tcpdump とは異なり TCP ストリームを再構築して解析できるパケットキャプチャツールです。

CentOS 7 なら epel から入れられます。

sudo yum -y install epel-release
sudo yum -y install tcpflow

CentOS 8 だと CERT Forensics Tools から入れられます。

sudo yum -y install https://forensics.cert.org/cert-forensics-tools-release-el8.rpm
sudo yum -y install tcpflow

Fedora 31 なら標準のリポジトリから入れられます。

sudo yum -y install tcpflow

tcpdump でキャプチャしたファイルを読むと、保存されたすべての TCP ストリームが表示されます。

tcpflow -c -r a.cap

直接キャプチャすれば TCP ストリームをリアルタイム表示できます。

tcpflow -c port 80

tcpdump からパイプもできます。

tcpdump -nn port 80 -w - | tcpflow -c -r -

さいごに

いつも tcpdump でキャプったファイルをローカルに落としてから GUI の wireshark で開いていました・・なぜ今まで tshark や tcpflow のことを知らなかったのか・・今思うとものすごい面倒なことしてました。

firewalld の target の default と REJECT の違い

firewalld でインタフェースやソースアドレスに基づいて特定のゾーンに入ったパケットが、そのゾーンに設定されているサービスやポートにマッチしなかったときのデフォルトの動作は、ゾーンの target で指定します。

指定します、と言っても定義済のゾーンでは次のように定義されています。これを変更することはあまりないと思います。

zone target
drop DROP
trusted ACCEPT
block %%REJECT%%
他のゾーン default

ACCEPT はパケットを許可、DROP はパケットをドロップします。

%%REJECT%%(以下 REJECT と記述します)と default は、どちらも ICMP destination unreachable かなにかで拒否を応答しますが、以下の記述だけを見ると REJECTdefault に違いは無いように思えます。

The %%REJECT%% target is used in block zone to reject (with default firewalld reject type) every packet not matching any rule.

https://firewalld.org/documentation/zone/options.html

がしかし、実際には下記の issue で説明されているような違いがあります。

https://github.com/firewalld/firewalld/issues/590

要約すると、

  1. default は icmp が通る
    • REJECT は icmp は明示的に通さない限り通らない
  2. A -> B な forward で A のゾーンが default なら B のゾーンも適用される
    • A のゾーンが REJECT なら B は適用されずに Reject される
  3. AllowZoneDrifting が有効なら)ソースアドレスベースのゾーンで default になれば、インタフェースベースのゾーンも続けて適用される
    • ソースベースのゾーンで REJECT になれが、インタフェースベースのゾーンは適用されずに Reject される

最後の AllowZoneDrifting について、以下によると元々はこれがデフォの動きだったのですが、

https://firewalld.org/2020/01/allowzonedrifting

0.6.5/0.7.0/0.8.0 でこれはバグ扱いで無効になりました。ゾーンベースの FW ではパケットが複数のゾーンを通るべきではないためらしいです。その後、0.7.4/0.8.2/1.0.0 で AllowZoneDrifting でオプショナルな機能になりました(デフォは無効)。

リージョン間の VPC ピアリング接続経由で SES SMTP の VPC endpoint を使う素振り

下記によると Amazon SES の SMTP エンドポイントに VPC エンドポイント(Private Link)経由でアクセスできるようになったので、インターネットアクセスの無いプライベートサブネットからでも VPC エンドポイント経由で SMTP でメールを送れるようになったようです。

もうこれでプライベートサブネットに配置した EC2 インスタンスからメールを送るためだけに NAT ゲートウェイを作る必要はなくなりました。

ただ、相変わらず SES は東京リージョンでは利用できず、SMTP の VPC エンドポイントも東京リージョンには作成できないため、東京リージョンの VPC と SES が利用可能なリージョン(オレゴン)の VPC をピアリング接続し、リージョン間 VPC ピアリング接続を経由して SES SMTP のエンドポイントを使ってみました。

環境は Terraform で作りました。残骸はこちら

bastion インスタンスやそもそも Internet Gateway すら作っていませんが、SSM Session Manager でログインできるので session-manager-plugin のインストールや ~/.ssh/config の設定が終わっていれば下記のようにインスタンス ID でログインできます。

env AWS_REGION=us-west-2      ssh i-xxxxxxxxxxxxxxxxx
env AWS_REGION=ap-northeast-1 ssh i-xxxxxxxxxxxxxxxxx

オレゴン(us-west-2)のインスタンスからメール送信

まずはオレゴン(us-west-2)のインスタンスに SSH ログインし、VPC エンドポイントにアクセスしてみます。

dig email-smtp.us-west-2.amazonaws.com
#=> email-smtp.us-west-2.amazonaws.com. 60 IN A     10.200.100.121

curl telnet://email-smtp.us-west-2.amazonaws.com:587
#=> 220 email-smtp.amazonaws.com ESMTP SimpleEmailService-d-xxxxxxxxx xxxxxxxxxxxxxxxxxxxx

名前解決の結果がプライベートアドレスになっており、587 ポートへの接続も成功しました。

postfix を設定してメールを送ってみます。

sudo postconf -e "relayhost = [email-smtp.us-west-2.amazonaws.com]:587" \
"smtp_sasl_auth_enable = yes" \
"smtp_sasl_security_options = noanonymous" \
"smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd" \
"smtp_use_tls = yes" \
"smtp_tls_security_level = encrypt" \
"smtp_tls_note_starttls_offer = yes"

# $SMTP_USERNAME と $SMTP_PASSWORD には SMTP のクレデンシャルが入っています
echo "[email-smtp.us-west-2.amazonaws.com]:587 $SMTP_USERNAME:$SMTP_PASSWORD" |
  sudo tee /etc/postfix/sasl_passwd >/dev/null

sudo postmap hash:/etc/postfix/sasl_passwd
sudo chown root:root /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
sudo chmod 0600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
sudo systemctl restart postfix

sendmail -t -foreore@example.com <<EOS
To: oreore@example.com
From: oreore@example.com
Subject: this is test

this is test
.
EOS

sudo tail -f /var/log/maillog

送信したメールが受信できることを確認しました。このインスタンスはインターネットアクセスが不可能で、yum update も素のままではできませんが、VPC エンドポイント経由でメールを送ることができました。

東京(ap-northeast-1)のインスタンスからメール送信

次に東京(ap-northeast-1)のインスタンスに SSH ログインし、VPC エンドポイントにアクセスしてみます。

dig email-smtp.us-west-2.amazonaws.com
#=> email-smtp.us-west-2.amazonaws.com. 60 IN A     52.40.152.63
#=> email-smtp.us-west-2.amazonaws.com. 60 IN A     52.32.157.220
#=> email-smtp.us-west-2.amazonaws.com. 60 IN A     34.214.66.156
#=> email-smtp.us-west-2.amazonaws.com. 60 IN A     44.233.4.146
#=> email-smtp.us-west-2.amazonaws.com. 60 IN A     35.160.245.254
#=> email-smtp.us-west-2.amazonaws.com. 60 IN A     35.161.82.107

名前解決の結果がパブリックアドレスになってしまいました。

VPC エンドポイントで Private DNS names enabled は有効にしているし、ピアリング接続で DNS resolution from accepter VPC to private IPDNS resolution from requester VPC to private IP も有効になっているのですが、ピアリング接続の先の VPC エンドポイントの Private DNS name の名前解決はできないようです。

次のように VPC エンドポイント固有の DNS 名を使えばプライベートアドレスが返ってきました。

dig vpce-xxxxxxxxxxxxxxxxx-xxxxxxxx.email-smtp.us-west-2.vpce.amazonaws.com
#=> vpce-xxxxxxxxxxxxxxxxx-xxxxxxxx.email-smtp.us-west-2.vpce.amazonaws.com. 60 IN A 10.200.100.121

curl telnet://vpce-xxxxxxxxxxxxxxxxx-xxxxxxxx.email-smtp.us-west-2.vpce.amazonaws.com:587
#=> 220 email-smtp.amazonaws.com ESMTP SimpleEmailService-d-xxxxxxxxx xxxxxxxxxxxxxxxxxxxx

名前解決の結果がプライベートアドレスになっており、587 ポートへの接続も成功しました。

ただ、こんなホスト名では TLS でホスト名の検証が通らないのではと思ったのですが、

openssl s_client -starttls smtp \
    -connect vpce-xxxxxxxxxxxxxxxxx-xxxxxxxx.email-smtp.us-west-2.vpce.amazonaws.com:587 \
    2>/dev/null \
  | openssl x509 -noout -text \
  | grep -A1 'X509v3 Subject Alternative Name'
#=> X509v3 Subject Alternative Name:
#=>   DNS:email-smtp-fips.us-west-2.amazonaws.com, DNS:*.email-smtp.us-west-2.vpce.amazonaws.com, DNS:email-smtp.us-west-2.amazonaws.com

SANs で DNS:*.email-smtp.us-west-2.vpce.amazonaws.com があるので大丈夫なようです。

postfix を設定してメールを送ってみます。 email-smtp.us-west-2.amazonaws.com だとダメなので vpce から始まる VPC エンドポイント固有の DNS 名を使う必要があります($SMTP_HOSTNAME 環境変数に入れてます)。

sudo postconf -e "relayhost = [$SMTP_HOSTNAME]:587" \
"smtp_sasl_auth_enable = yes" \
"smtp_sasl_security_options = noanonymous" \
"smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd" \
"smtp_use_tls = yes" \
"smtp_tls_security_level = encrypt" \
"smtp_tls_note_starttls_offer = yes"

echo "[$SMTP_HOSTNAME]:587 $SMTP_USERNAME:$SMTP_PASSWORD" |
  sudo tee /etc/postfix/sasl_passwd >/dev/null

sudo postmap hash:/etc/postfix/sasl_passwd
sudo chown root:root /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
sudo chmod 0600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
sudo systemctl restart postfix

sendmail -t -foreore@example.com <<EOS
To: oreore@example.com
From: oreore@example.com
Subject: this is test

this is test
.
EOS

sudo tail -f /var/log/maillog

送信したメールが受信できることを確認しました。このインスタンスはインターネットアクセスが不可能で、しかも SES が利用できない東京リージョンですが、ピアリング接続経由の VPC エンドポイント経由でメールを送ることができました。

さいごに

主要なサービスはだいたい VPC エンドポイントが作れるようになったし、SSH も SSM の VPC エンドポイントがあればプライベートサブネットでも直に接続できるので、もう踏み台や NAT は不要そうです。

TCP backlog が溢れたときに何が起こるか

何となくふんわりとしか理解していなくて、実際のところ TCP backlog が溢れたときに Client~Server 間で何が起こるかよく判っていなかったので、実際に backlog が 1 で accept しない簡易な TCP サーバを作り、tcpdump でどのようなパケットが飛び交うかを覗いて確認しました。


TCP 3 way handshake では次のような段階で状態遷移が起こる(併記している TCP 状態はパケットを投げた時点のもの)。

1. Client<SYN-SENT>]   --- [SYN]     --> Server<LISTEN>
2. Client<SYN-SENT>]   <-- [SYN/ACK] --- Server<SYN-RECV>
3. Client<ESTABLISHED> ---     [ACK] --> Server<SYN-RECV>
4. Client<ESTABLISHED>                   Server<ESTABLISHED>

backlog が溢れると、3. のクライアントからサーバへの ACK をサーバがドロップする。

サーバから見ると SYN-RECV になった後の ACK が来ていない状態なので 2. の SYN/ACK を 1 秒後に再送します(昔は 3 秒だった気がするのだけどいつからか 1 秒に変わっているらしい)。

クライアントが生きていればその SYN/ACK に対する 3. の ACK も再び送られるものの、サーバの backlog に空きができていなければ再びドロップされる。

サーバは net.ipv4.tcp_synack_retries の回数まで間隔を倍々しながら SYN/ACK を再送する。net.ipv4.tcp_synack_retries=5 なら 1,2,4,8,16 秒後に SYN/ACK が再送された後、32秒後に SYN-RECV も消える(syn backlog から消える)。

一方でクライアントは最後の ACK を投げた時点で接続が確立(ESTABLISHED)している(つもりになっている)。なので何事もなければサーバからのデータをいつまでも待ち続けるし、クライアントから最初にデータを送るプロトコルなのであれば(HTTP とか)クライアントから最初のデータが送信される。クライアントからデータが投げられた場合、サーバが SYN-RECV であればそれはドロップされるため、クライアントは同じデータを再送する。再送間隔は RTO で決まるので可変。再送回数は や net.ipv4.tcp_retries2 で決まる(+3される?)。

なお、クライアントが TCP 接続を閉じた場合はクライアントから FIN が送られるもののサーバが SYN-RECV ならこの FIN もドロップされるため、クライアントから FIN が再送される。

net.ipv4.tcp_abort_on_overflow=1

上記は net.ipv4.tcp_abort_on_overflow=0 の場合の動作。

サーバで net.ipv4.tcp_abort_on_overflow=1 にすると backlog が溢れると ACK を DROP するのではなく、Reject(RST)するようになる。のでクライアントは直ちに Connection reset になるので、再送も起こらない。

さいごに

TCP syn backlog が溢れたときの動きもそのうち見てみる。

Makefile で複数の Lambda 関数をデプロイするメモ

複数の Lambda 関数で構成される環境を、基本的な構成は Terraform で管理しつつ、Lambda 関数のコードは make で aws cli を呼び出してデプロイするメモ。

Lambda 関数のコードも含めてすべてを Terraform で管理することもできると思いますが・・・それだとコードを修正したときのデプロイに無駄に時間が掛かりすぎて辛いので却下。

Terraform での Lambda 関数の定義

Lambda 関数の作成は Terraform でやりますが、コードの更新は aws cli を使うので、Terraform でコードの内容の変化は無視しておく必要があります。

locals {
  # ダミーのファイル(空で良い)
  dummy_file = {
    filename         = "dummy.zip"
    source_code_hash = filebase64sha256("dummy.zip")
  }
}

resource "aws_lambda_function" "aaa" {
  function_name = "aaa"
  role          = aws_iam_role.lambda.arn
  handler       = "index.aaa"
  runtime       = "nodejs12.x"

  filename         = local.dummy_file.filename
  source_code_hash = local.dummy_file.source_code_hash

  # filename と source_code_hash を無視する
  lifecycle {
    ignore_changes = [
      filename,
      source_code_hash,
    ]
  }
}

Makefile

make で Lambda 関数のコードのアップロードや、ログの表示ができます。

# Lambda 関数にアップロードするアーカイブを作成
make build

# すべての Lambda 関数にアーカイブをアップロード
make deploy

# 特定の Lambda 関数にアーカイブをアップロード
make deploy/aaa

# すべての Lambda 関数のログを aws logs tail --follow で表示
# 複数のロググループを並列に tail --follow するために -j で並列処理させる
make logs -j

# 特定の Lambda 関数のログを aws logs tail --follow で表示
make logs/aaa

make logs/aaa などで指定している aaa は make のターゲットで指定しやすくするためのエイリアスのようなもので envs/dev.mk などの環境ごとのファイルで次のように定義しています。IAM クレデンシャルのプロファイルを切り替えるために aws コマンドもここで指定しています。

# aws コマンド(対象となる環境によってプロファイルを切り替える)
AWS := aws --profile oreore

# make のターゲットで使用する名前のリスト
TARGETS := aaa bbb ccc

# ↑のターゲット名と実際の Lambda 関数の名前のマッピング
LAMBDA_aaa := test-dev-lambda-aaa
LAMBDA_bbb := test-dev-lambda-bbb
LAMBDA_ccc := test-dev-lambda-ccc

Makefile は次のような内容です。

# 対象となる環境ごとの定義ファイルをインクルード
include envs/$(APP_ENV).mk

# ソースファイル
# 直下の *.js と src ディレクトリ内の *.js
# node_modules は含めません
SRC_FILES := $(wildcard *.js) $(shell find src -name '*.js')

# ビルド用ディレクトリ
# アップロード用のアーカイブやデプロイ結果が保存されます
BUILD_DIR     := .build
BUILD_ENV_DIR := $(BUILD_DIR)/$(APP_ENV)

.PHONY: all
all: deploy

.PHONY: build
build: $(BUILD_DIR)/package.zip

# lambda にアップロードするアーカイブを作成します
# node_modules だけ含むアーカイブをコピーしてソースファイルを追加します
# 変数 $@ や $< で書いたら見辛い気がしたのであえて使ってません
$(BUILD_DIR)/package.zip: $(BUILD_DIR)/node_modules.zip $(SRC_FILES)
mkdir -p $(BUILD_DIR)
    cp $(BUILD_DIR)/node_modules.zip $(BUILD_DIR)/package~.zip
    zip -r $(BUILD_DIR)/package~.zip $(SRC_FILES)
    mv $(BUILD_DIR)/package~.zip $(BUILD_DIR)/package.zip

# node_modules だけ含むアーカイブを作成します
# package.json や package-lock.json が更新されたときだけリビルドします
# devDependencies は含めたくないので npm ci --prod してアーカイブ化した後に npm i で戻します
$(BUILD_DIR)/node_modules.zip: package.json package-lock.json
    mkdir -p $(BUILD_DIR)
    npm ci --prod
    rm -f $(BUILD_DIR)/node_modules.zip
    zip -r $(BUILD_DIR)/node_modules.zip node_modules
    npm i

# すべての Lambda 関数をデプロイ、次のように展開されます
#   deploy: deploy/aaa deploy/bbb deploy/ccc
.PHONY: deploy
deploy: $(TARGETS:%=deploy/%)

# 特定の Lambda 関数をデプロイ、次のように展開されます
#   deploy/aaa: .build/dev/aaa/deploy.json
#   deploy/bbb: .build/dev/bbb/deploy.json
#   deploy/ccc: .build/dev/ccc/deploy.json
# ただのワイルドカードパターンだといろいろ不都合があったので、
# 静的パターンルールで記述します
.PHONY: $(TARGETS:%=deploy/%)
$(TARGETS:%=deploy/%): deploy/%: $(BUILD_ENV_DIR)/%/deploy.json

# .build/dev/aaa/deploy.json のようなターゲット名から、
# ワイルドカード部分の aaa を取り出して TARGET 変数に入れます
$(BUILD_ENV_DIR)/%/deploy.json: TARGET = $*
# LAMBDA_aaa などの変数から Lambda 関数名を取得して LAMBDA 変数に入れます
$(BUILD_ENV_DIR)/%/deploy.json: LAMBDA = $(LAMBDA_$(TARGET))
# アップロードを実行して成功したら .build/dev/aaa/deploy.json のようなファイルを作成します
$(BUILD_ENV_DIR)/%/deploy.json: $(BUILD_DIR)/package.zip
    mkdir -p $(@D)
    $(AWS) lambda update-function-code --function-name $(LAMBDA) --zip-file fileb://$< > $@~
    mv $@~ $@

# すべての Lambda 関数のログを tail -f します
# make -j で並列実行させないと一つのログしか tail -f できません
.PHONY: logs
logs: $(TARGETS:%=logs/%)

.PHONY: $(TARGETS:%=logs/%)
# logs/aaa のような名前から TARGET に aaa を入れます
logs/%: TARGET = $*
# LAMBDA_hoge などの変数から Lambda 関数名を取得して LAMBDA 変数に入れます
logs/%: LAMBDA = $(LAMBDA_$(TARGET))
# 特定の Lambda 関数のログを tail -f します(Ctrl+C するまで続行)
# ただのワイルドカードパターンだと .PHONY にできないので、
# 静的パターンルールで記述します
$(TARGETS:%=logs/%): logs/%: deploy/%
    $(AWS) logs tail "/aws/lambda/$(LAMBDA)" --follow --format=short --since 1s

# tmux でペイン分割して複数のログを tail -f します
# Lambda 関数が多くなると見辛いので基本的には↑を使ってます
# 動的にコマンドを生成するために Makefile の foreach 関数使ってます
.PHONY: tmux-logs
tmux-logs: deploy
    tmux new : \; $(foreach t,$(TARGETS), split make logs/$(t) \; ) set sync \; select-layout main-v

.PHONY: clean
clean:
    rm -fr $(BUILD_DIR)

補足とか

Serverless Framework

複数の Lambda 関数のデプロイを管理するなら apex が便利かと思ってたんですけど こういう状況になっているので もう使わないほうが良さそうです。

代替としては Serverless Framework とか? ただこれは CloudFormation を使うようです。今回は自社管理外の AWS 環境にちょこっと Lambda 関数をデプロイするだけのものなのであまり大げさなことはやりたくありませんでした。

Lambda Layer

デプロイの都度 node_modules がまるごとアップロードされているのと、すべての Lambda 関数に同じ node_modules が含まれた状態になっていて無駄なので、node_modules は Lambda layer にして共有するのが良いかも。

さいごに

Makefile 毎回書き方ググってる気がするので未来の自分用にメモ。

tmux で複数コマンドを複数ペインでさっと実行する

だいぶ前に社内ブログで次のような方法で tmux でペイン分割して複数コマンドを同時に実行する方法を紹介していたのですが、

tmux new-session -d ping 1.1.1.1 &&\
tmux split-window ping 8.8.8.8 &&\
tmux split-window ping 8.8.4.4 &&\
tmux set-window-option synchronize-panes &&\
tmux select-layout even-vertical &&\
tmux attach

asciicast

がしかし tmux のコマンドを覚えなくてももっと簡単にできるラッパーがあります。

xpanes

xargs っぽい感覚で tmux でペイン分割できるラッパーです。機能はいろいろ豊富ですが次のようにすれば↑でやろうとしていたこととだいたい同じことができます。

xpanes -ss -e 'ping 1.1.1.1' 'ping 8.8.8.8' 'ping 8.8.4.4'

asciicast

これは便利!

しかもこのコマンド epel からサクッとインストールできます。

sudo dnf -y install epel-release
sudo dnf -y install xpanes

これは楽ちん!

がしかしコマンドが開始されるまでが異様に遅いです。tmux を直接叩けばもっとサクッと動くのですが・・・

tmux-cmds

xpanes だとコマンドが開始されるまで異様に遅かったので、もっと簡単なラッパーを作ってみたのがこれ。

tmux-cmds ping 1.1.1.1 :: ping 8.8.8.8 :: ping 8.8.4.4

asciicast

これは便利!!

ただのシェルスクリプトなのでパスを通したところにダウンロードすれば OK です。

curl -L https://raw.githubusercontent.com/ngyuki/dotfiles/master/bin/tmux-cmds -o "$HOME/bin/tmux-cmds"
chmod +x "$HOME/bin/tmux-cmds"

これは・・・yum とか dnf で入れられるのと比べると楽ちんではないですね。

tmux multiple commands

tmux-cmds を作るために tmux の man を見ていたら、tmux の複数コマンドを一撃で指定できることを知りました。

なので、最初の tmux を直接使う版は次のように変更できます。

tmux new-session ping 1.1.1.1 \;\
    split-window ping 8.8.8.8 \;\
    split-window ping 8.8.4.4 \;\
    set-window-option synchronize-panes \;\
    select-layout even-vertical

asciicast

tmux のコマンドには短いエイリアスがあるのと、コマンドやオプションは前方一致で候補が一つなら自動的にそれが指定されたことになるので、次のようにも書けます。

tmux new ping 1.1.1.1 \; split ping 8.8.8.8 \; split ping 8.8.4.4 \; set sync \; selectl even-v

asciicast

これは便利!!! ラッパーとか使わなくてもこれで十分では感があります(この投稿のモチベーション)。

もう少し削れますが、これはむしろわかりにくいですね。

tmux new ping 1.1.1.1 \; sp ping 8.8.8.8 \; sp ping 8.8.4.4 \; set sy \; selectl even-v

CentOS 8 を virt-install でサクッと入れる

こんな感じに centos8.ks.cfg を作成して、

#version=RHEL8

cmdline
url --url=http://ftp.riken.jp/Linux/centos/8.1.1911/BaseOS/x86_64/os/

lang en_US.UTF-8
keyboard --vckeymap=jp --xlayouts=jp
timezone Asia/Tokyo --isUtc --ntpservers=192.168.2.2

network --activate --device=link --noipv6 --bootproto=dhcp

zerombr
clearpart --all --initlabel
bootloader --location=mbr --boot-drive=vda
part / --label=root --grow --asprimary --ondisk=vda

rootpw --plaintext password
skipx
selinux --disabled
firewall --disabled
firstboot --disabled

services --enabled=chronyd --disabled=kdump,auditd

poweroff

%packages
@^minimal-environment
%end

CentOS-8.1.1911-x86_64-boot.iso を適当なディレクトリにダウンロードした後、次のようにサクッと作成できます。

# ダウンロード
wget http://ftp.riken.jp/Linux/centos/8.1.1911/isos/x86_64/CentOS-8.1.1911-x86_64-boot.iso -O /iso/CentOS-8.1.1911-x86_64-boot.iso

# 論理ボリューム作成
lvcreate vg99 -L 8G -n vm.centos8

# セットアップ
virt-install \
  --name centos8 \
  --hvm \
  --virt-type kvm \
  --ram 2048 \
  --vcpus 1 \
  --arch x86_64 \
  --os-type linux \
  --os-variant rhel8.1 \
  --boot hd \
  --disk path=/dev/vg99/vm.centos8,bus=virtio \
  --network network=default,model=virtio \
  --graphics none \
  --serial pty \
  --console pty \
  --location /iso/CentOS-8.1.1911-x86_64-boot.iso \
  --initrd-inject /tmp/centos8.ks.cfg \
  --extra-args "inst.ks=file:/centos8.ks.cfg inst.stage2=cdrom: console=ttyS0 net.ifnames=0 biosdevname=0" \
  --noreboot

# 起動してコンソールに接続
virsh start centos8
virsh console centos8

RPM パッケージが含まれない CentOS-8.1.1911-x86_64-boot.iso を使っているので、パッケージのダウンロードのためにやや時間かかります。

CentOS-8.1.1911-x86_64-dvd1.iso を使う場合は ks.cfgurl の行を cdrom とかに変更すればたぶん大丈夫です。

ダメだった

CentOS 7.7 の virt-install だと↑でサクッと入れられたのですが、CentOS 8.1 の virt-install だとダメでした。

ERROR    Error validating install location: Could not find an installable distribution at '/iso/CentOS-8.1.1911-x86_64-boot.iso'

The location must be the root directory of an install tree.
See virt-install man page for various distro examples.

virt-install --debug とかで見てみたところ、CentOS-8.1.1911-x86_64-boot.iso のメディアに .treeinfo が無いことが原因のようです。

--location ではなく --cdrom ならこのエラーは出ませんが、それだと --initrd-inject--extra-args が効かないのでキックスタートさせるのが面倒になります。

試していませんが CentOS-8.1.1911-x86_64-dvd1.iso なら .treeinfo が含まれているので大丈夫です。

解決方法

.treeinfo から initrd.imgvmlinuz の位置を読んでいるだけなので、--location でそれらの位置も一緒に指定すれば OK です。

virt-install \
  --name centos8 \
  --hvm \
  --virt-type kvm \
  --ram 2048 \
  --vcpus 1 \
  --arch x86_64 \
  --os-type linux \
  --os-variant rhel8.1 \
  --boot hd \
  --disk path=/dev/vg99/vm.centos8,bus=virtio \
  --network network=default,model=virtio \
  --graphics none \
  --serial pty \
  --console pty \
  --location /iso/CentOS-8.1.1911-x86_64-boot.iso,kernel=isolinux/vmlinuz,initrd=isolinux/initrd.img \
  --initrd-inject /tmp/centos8.ks.cfg \
  --extra-args "inst.ks=file:/centos8.ks.cfg inst.stage2=cdrom: console=ttyS0 net.ifnames=0 biosdevname=0" \
  --noreboot

別の解決方法

man virt-install したら↑の --locationkernelinitrd を指定する方法が書いていたのでこれでええやんと思ったんですが、それに気づく前に試行錯誤したメモ。

CentOS-8.1.1911-x86_64-boot.iso.treeinfo を追加したメディアを作れば大丈夫です。

mount /iso/CentOS-8.1.1911-x86_64-boot.iso /mnt -o loop
rsync -av /mnt/ /tmp/CentOS-8.1.1911-x86_64-boot/
curl http://ftp.riken.jp/Linux/centos/8.1.1911/BaseOS/x86_64/os/.treeinfo > /tmp/CentOS-8.1.1911-x86_64-boot/.treeinfo
mkisofs \
  -o /iso/CentOS-8.1.1911-x86_64-boot-x.iso \
  -b isolinux/isolinux.bin \
  -c isolinux/boot.cat \
  -no-emul-boot \
  -boot-load-size 4 \
  -boot-info-table \
  -r -J -v \
  /tmp/CentOS-8.1.1911-x86_64-boot/

rm -fr /tmp/CentOS-8.1.1911-x86_64-boot/
umount /mnt

virt-install \
  --name centos8 \
  --hvm \
  --virt-type kvm \
  --ram 2048 \
  --vcpus 1 \
  --arch x86_64 \
  --os-type linux \
  --os-variant rhel8.1 \
  --boot hd \
  --disk path=/dev/vg99/vm.centos8,bus=virtio \
  --network network=default,model=virtio \
  --graphics none \
  --serial pty \
  --console pty \
  --location /iso/CentOS-8.1.1911-x86_64-boot-x.iso \
  --initrd-inject /tmp/centos8.ks.cfg \
  --extra-args "inst.ks=file:/centos8.ks.cfg inst.stage2=cdrom: console=ttyS0 net.ifnames=0 biosdevname=0" \
  --noreboot

あるいは ISO は使わずに --location も URL を指定すれば大丈夫です。ただこの方法だと images/install.img のようなでかいファイル(509M)までダウンロードされるのでめちゃくちゃ時間かかります。

virt-install \
  --name centos8 \
  --hvm \
  --virt-type kvm \
  --ram 2048 \
  --vcpus 1 \
  --arch x86_64 \
  --os-type linux \
  --os-variant rhel8.1 \
  --boot hd \
  --disk path=/dev/vg99/vm.centos8,bus=virtio \
  --network network=default,model=virtio \
  --graphics none \
  --serial pty \
  --console pty \
  --location http://ftp.riken.jp/Linux/centos/8.1.1911/BaseOS/x86_64/os/ \
  --initrd-inject /tmp/centos8.ks.cfg \
  --extra-args "inst.ks=file:/centos8.ks.cfg console=ttyS0 net.ifnames=0 biosdevname=0" \
  --noreboot