SSHをプロキシするsshpiperの紹介

sshpiper とは

sshpiper は https://github.com/tg123/sshpiper が開発しているSSLプロキシサーバです。SSHのユーザ名をキーにして別のSSHホストにプロキシすることができます。

最近のクラウド・仮想化によりサーバが直接インターネットに接続していない場合に有効そうだったので確認しました。 なお、インストール方法は、GitHub に丁寧に記載されていますので割愛します。

動作について

sshpiper 自身にはパスワード認証はなく、プロキシ先のサーバが存在してパスワード要求している場合にパスワード要求をプロキシします。 ただし、公開鍵暗号方式を利用する場合は、接続元ホストの公開鍵を sshpiper に設定し、sshpiper の秘密鍵を プロキシ先のホストに設定する必要があります。

ポートフォワーディングなどもプロキシするので、SSHを踏み台する場合だと複雑な工程になる省略することもできます。

CentOS7のLXCでCentOS6を非特権ユーザで起動させる

はじめに

この記事は、とある調査の結果をまとめたものであって、一般には参考にならない内容になっているのでご注意ください。

CentOS 7で特権ユーザ

CentOS 7の epel に含まれる lxc 1.0.11 では、非特権機能がまともに動きません。素直に lxc-3.0.x の最新安定版のソースコードを公式からダウンロードしてビルド [1] します。

bridge の設定をします。

# yum install bridge-utils
# brctl addbr lxcbr0

非特権ユーザの設定をします。

/etc/subuid
root:100000:65536
/etc/subgid
root:100000:65536
/etc/lxc/lxc-usernet
root veth lxcbr0 10

CentOS 6は、initプロセスが大きく違うので、手始めに CentOS 7 のコンテナを作成して特権ユーザで起動できることを確認します。

# lxc-create -n centos7 -t centos -- -R 7

# lxc-ls -f
NAME      STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED
centos7   STOPPED 0         -      -    -    false

起動しようとすると、以下の No space left on device というエラーになります。

# lxc-start -n centos7 -F
lxc-start: centos7: cgroups/cgfsng.c: __do_cgroup_enter: 1498 No space left on device - Failed to enter cgroup "/sys/fs/cgroup/cpuset//lxc.monitor/centos7/cgroup.procs"
lxc-start: centos7: start.c: __lxc_start: 1992 Failed to enter monitor cgroup
lxc-start: centos7: tools/lxc_start.c: main: 329 The container failed to start
lxc-start: centos7: tools/lxc_start.c: main: 335 Additional information can be obtained by setting the --logfile and --logpriority options

/sys/fs/cgroup の容量は十分に空いています。原因を調べたところ以下のフラグを 1 にする必要がありました。

echo 1 >> `/sys/fs/cgroup/cpuset/cgroup.clone_children`

ただし、必ず lxc-start を実行する前に指定しなければいけません。もし、1度でも lxc-start でコンテナを起動してしまった場合、cgroup が不完全な状態で作成されてしまうので二度と起動 [2] できなくなります。

事実、2回目以降はエラーは以下のように File exists なります。

lxc-start: centos7: cgroups/cgfsng.c: mkdir_eexist_on_last: 1277 File exists - Failed to create directory "/sys/fs/cgroup/cpuset//lxc.monitor/centos7"
lxc-start: centos7: cgroups/cgfsng.c: monitor_create_path_for_hierarchy: 1298 Failed to create cgroup "/sys/fs/cgroup/cpuset//lxc.monitor/centos7"
lxc-start: centos7: cgroups/cgfsng.c: cgfsng_monitor_create: 1388 Failed to create cgroup "/sys/fs/cgroup/cpuset//lxc.monitor/centos7"
lxc-start: centos7: cgroups/cgfsng.c: __do_cgroup_enter: 1498 No space left on device - Failed to enter cgroup "/sys/fs/cgroup/cpuset//lxc.monitor/centos7-1/cgroup.procs"
lxc-start: centos7: start.c: __lxc_start: 1992 Failed to enter monitor cgroup
lxc-start: centos7: tools/lxc_start.c: main: 329 The container failed to start
lxc-start: centos7: tools/lxc_start.c: main: 335 Additional information can be obtained by setting the --logfile and --logpriority options

これで CentOS 7 が起動できました。

# brctl addbr lxcbr0
# echo 1 >> `/sys/fs/cgroup/cpuset/cgroup.clone_children`

# lxc-start --name centos7 -F
lxc-start: centos7: start.c: proc_pidfd_open: 1607 Function not implemented - Failed to send signal through pidfd
systemd 219 running in system mode. (+PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 -SECCOMP +BLKID +ELFUTILS +KMOD +IDN)
Detected virtualization lxc.
Detected architecture x86-64.

Welcome to CentOS Linux 7 (Core)!

CentOS 7で非特権ユーザ

コンテナが特権か非特権か?は、 lxc-ls コマンド結果の UNPRIVILEGED で確認することができます。 現在は false となっており特権ユーザであることを表しています。

# lxc-ls -f
NAME      STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED
centos7   STOPPED 0         -      -    -    false

非特権ユーザにするには、コンテナの設定ファイルに以下の2行を追記します。

/usr/local/var/lib/lxc/centos7/config
lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536

設定が正しければ UNPRIVILEGEDtrue に変わります。

# lxc-ls -f
NAME      STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED
centos7   STOPPED 0         -      -    -    false
Tip
コンテナが起動中の場合は true になりません。一度コンテナを停止させます。

起動しようとすると、以下の Failed to clone process in new user namespace というエラーになります。 原因は、CentOS 7 の現在の最新のカーネル 3.10.0-1062.12.1.el7.x86_64 では、非特権ユーザに対応していないためです。

# lxc-start -n centos7
lxc-start: centos7: cgroups/cgfsng.c: mkdir_eexist_on_last: 1277 File exists - Failed to create directory "/sys/fs/cgroup/systemd//lxc.payload/centos7"
lxc-start: centos7: cgroups/cgfsng.c: container_create_path_for_hierarchy: 1317 Failed to create cgroup "/sys/fs/cgroup/systemd//lxc.payload/centos7"
lxc-start: centos7: cgroups/cgfsng.c: cgfsng_payload_create: 1454 Failed to create cgroup "/sys/fs/cgroup/systemd//lxc.payload/centos7"
lxc-start: centos7: start.c: lxc_spawn: 1737 Invalid argument - Failed to clone a new set of namespaces
lxc-start: centos7: start.c: __lxc_start: 2019 Failed to spawn container "centos7"
lxc-start: centos7: conf.c: userns_exec_1: 4311 Failed to clone process in new user namespace
lxc-start: centos7: tools/lxc_start.c: main: 329 The container failed to start
lxc-start: centos7: tools/lxc_start.c: main: 335 Additional information can be obtained by setting the --logfile and --logpriority options

そのため、カーネルを 3.12 以上に上げます。CentOS 7には、都合のいいバージョンは落ちていないため elrepo で最新のカーネルをインストールします。

# rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
# rpm -Uvh http://www.elrepo.org/elrepo-release-7.0-2.el7.elrepo.noarch.rpm
# yum install --enablerepo=elrepo-kernel kernel-ml

僕の場合は 5.5.5-1 が落ちてきたので、OS再起動してカーネルを変更してからもう一度コンテナを起動させます。 しかし、rootfs が Permission denied となり、まだ起動できません。

# uname -r
5.5.5-1.el7.elrepo.x86_64

# brctl addbr lxcbr0
# echo 1 >> `/sys/fs/cgroup/cpuset/cgroup.clone_children`

# lxc-start --name centos7 -F
lxc-start: centos7: storage/dir.c: dir_mount: 198 Permission denied - Failed to mount "/usr/local/var/lib/lxc/centos7/rootfs" on "/usr/local/lib/lxc/rootfs"
lxc-start: centos7: conf.c: lxc_mount_rootfs: 1328 Failed to mount rootfs "/usr/local/var/lib/lxc/centos7/rootfs" onto "/usr/local/lib/lxc/rootfs" with options "(null)"
lxc-start: centos7: conf.c: lxc_setup_rootfs_prepare_root: 3393 Failed to setup rootfs for
lxc-start: centos7: conf.c: lxc_setup: 3496 Failed to setup rootfs
lxc-start: centos7: start.c: do_start: 1299 Failed to setup container "centos7"
lxc-start: centos7: sync.c: __sync_wait: 62 An error occurred in another process (expected sequence number 5)
lxc-start: centos7: start.c: lxc_abort: 1103 Function not implemented - Failed to send SIGKILL to 2809
lxc-start: centos7: start.c: __lxc_start: 2019 Failed to spawn container "centos7"
lxc-start: centos7: tools/lxc_start.c: main: 329 The container failed to start
lxc-start: centos7: tools/lxc_start.c: main: 335 Additional information can be obtained by setting the --logfile and --logpriority options

rootfs の上位ディレクトリの権限を変更して、他人でも閲覧できるようにします。

# ls -ld /usr/local/var/lib/lxc/centos7
drwxrwx--- 3 root root 55 Feb 22 20:48 /usr/local/var/lib/lxc/centos7

# chmod 775 /usr/local/var/lib/lxc/centos7

# ls -ld /usr/local/var/lib/lxc/centos7
drwxrwxr-x 3 root root 55 Feb 22 20:48 /usr/local/var/lib/lxc/centos7

これで CentOS 7 が非特権ユーザで起動できました。

# lxc-start --name centos7 -F
lxc-start: centos7: start.c: proc_pidfd_open: 1607 Function not implemented - Failed to send signal through pidfd
systemd 219 running in system mode. (+PAM +AUDIT +SELINUX +IMA -APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4 -SECCOMP +BLKID +ELFUTILS +KMOD +IDN)
Detected virtualization lxc.
Detected architecture x86-64.

Welcome to CentOS Linux 7 (Core)!

CentOS 6で非特権ユーザ

CentOS 6のコンテナを作成します。

# lxc-create -n centos6 -t centos -- -R 6

まだ、特権ユーザであるため lxc.idmap の設定を追記します。

/usr/local/var/lib/lxc/centos6/config
lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536

非特権ユーザにできたので、起動してみます。

# chmod 755 /usr/local/var/lib/lxc/centos6

# lxc-ls -f
NAME      STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED
centos6   STOPPED 0         -      -    -    true
centos7   RUNNING 0         -      -    -    true

# lxc-start --name centos6 -F
init: lxc-sysinit pre-start process (2) terminated with status 1
cat: /proc/self/attr/current: Invalid argument
                Welcome to CentOS

起動はできましたが、途中で止まってしまいます。init.d の時代は 非root で実行することを考慮していないためです。

とりあえず、起動スクリプトをすべて止めます。

# chroot /usr/local/var/lib/lxc/centos6/rootfs/

# chkconfig --list
crond           0:off   1:off   2:on    3:on    4:on    5:on    6:off
iptables        0:off   1:off   2:on    3:on    4:on    5:on    6:off
netconsole      0:off   1:off   2:off   3:off   4:off   5:off   6:off
netfs           0:off   1:off   2:off   3:on    4:on    5:on    6:off
network         0:off   1:off   2:on    3:on    4:on    5:on    6:off
rdisc           0:off   1:off   2:off   3:off   4:off   5:off   6:off
restorecond     0:off   1:off   2:off   3:off   4:off   5:off   6:off
rsyslog         0:off   1:off   2:on    3:on    4:on    5:on    6:off
saslauthd       0:off   1:off   2:off   3:off   4:off   5:off   6:off
sendmail        0:off   1:off   2:on    3:on    4:on    5:on    6:off
sshd            0:off   1:off   2:on    3:on    4:on    5:on    6:off
udev-post       0:off   1:on    2:off   3:off   4:off   5:off   6:off

# chkconfig crond off
# chkconfig iptables off
# chkconfig netfs off
# chkconfig network off
# chkconfig rsyslog off
# chkconfig sendmail off
# chkconfig sshd off

# echo > /etc/rc.d/rc.sysinit

不要な /dev/tty も消しておきます。これをやっておかないと1コンテナあたり無駄なプロセスが 6 個も起動してしまいます。

/etc/sysconfig/init
ACTIVE_CONSOLES=/dev/tty[1-6]
↓
ACTIVE_CONSOLES=

また /etc/mtab も作成する必要があります。自分で書くのが面倒だったので、特権ユーザで起動させて rc.sysinit で生成したものを利用しました。

/etc/mtab
# cat /etc/mtab
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
devtmpfs /dev devtmpfs rw,nosuid,size=474792k,nr_inodes=118698,mode=755 0 0
securityfs /sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0
tmpfs /dev/shm tmpfs rw,nosuid,nodev 0 0
devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0 0
tmpfs /run tmpfs rw,nosuid,nodev,mode=755 0 0
tmpfs /sys/fs/cgroup tmpfs ro,nosuid,nodev,noexec,mode=755 0 0
cgroup /sys/fs/cgroup/systemd cgroup rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd 0 0
pstore /sys/fs/pstore pstore rw,nosuid,nodev,noexec,relatime 0 0
cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory 0 0
cgroup /sys/fs/cgroup/blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio 0 0
cgroup /sys/fs/cgroup/net_cls,net_prio cgroup rw,nosuid,nodev,noexec,relatime,net_cls,net_prio 0 0
cgroup /sys/fs/cgroup/freezer cgroup rw,nosuid,nodev,noexec,relatime,freezer 0 0
cgroup /sys/fs/cgroup/pids cgroup rw,nosuid,nodev,noexec,relatime,pids 0 0
cgroup /sys/fs/cgroup/cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset,clone_children 0 0
cgroup /sys/fs/cgroup/cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0
cgroup /sys/fs/cgroup/rdma cgroup rw,nosuid,nodev,noexec,relatime,rdma 0 0
cgroup /sys/fs/cgroup/hugetlb cgroup rw,nosuid,nodev,noexec,relatime,hugetlb 0 0
cgroup /sys/fs/cgroup/perf_event cgroup rw,nosuid,nodev,noexec,relatime,perf_event 0 0
cgroup /sys/fs/cgroup/devices cgroup rw,nosuid,nodev,noexec,relatime,devices 0 0
configfs /sys/kernel/config configfs rw,relatime 0 0
/dev/mapper/centos-root / xfs rw,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota 0 0
systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=26,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=19458 0 0
debugfs /sys/kernel/debug debugfs rw,relatime 0 0
hugetlbfs /dev/hugepages hugetlbfs rw,relatime,pagesize=2M 0 0
mqueue /dev/mqueue mqueue rw,relatime 0 0
/dev/sda1 /boot xfs rw,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota 0 0
tmpfs /run/user/1000 tmpfs rw,nosuid,nodev,relatime,size=97784k,mode=700,uid=1000,gid=1000 0 0

これで CentOS 6 が非特権ユーザで起動できました。

# lxc-start --name centos6

お疲れ様でした。

補足

検証している中で LXC の動作が途中で失敗すると /sys/fs/cgroup にコンテナのゴミが残り再起動できなくなる問題が度々発生しました。Cgroups を操作すれば消すことはできると思うのですが調べていません。


1. LXC は Canonical社が支援しているだけあり RPM はありません。
2. OS再起動で直ります

MySQLのデータベースごとにディスク容量制限を簡単にしたい

モチベーション

MySQLのデータベース単位で quota のようなディスク容量制限をしたかったのですが、MySQL にはそういった機能がないようでした。 しかし、MySQL はデータベースごとにフォルダが別れているので、各フォルダごとにディスクをマウントすれば容量の制限ができると思い、 LVM で小さな領域を作成して、データベースのフォルダ位置にマウントしてみたところ、制限ができたのでこの方法を採用しました。

データベースを作成する度に、すべて手作業で実施していたらいつかはLVMの操作ミスでデータが喪失する恐れがありました。 また、レプリカしているDBの場合にレプリカ先にも同じマウントポイントを指定する必要があり、作業が漏れやすいという問題もありました。 そのため ansible のプレイブックを使って、すべての作業を 1コマンド で実行できるようにしました。

プレイブック

最新の ansible には、parted コマンド、LVM操作コマンド、mkfs系コマンド、mysql操作コマンドが用意されているので、これらを組み合わせた以下のプレイブックを作成します。

task.yml
---
# parted で 基本パーティション 1 を作成する
- name: partition
  parted:
    device: "{{ item }}"
    number: 1
    state: present
    flags: [ lvm ]
  with_items: "{{ userctl_disks }}"

# 物理ボリュームとボリュームグループを作成する
- name: create physical volume and volume group
  lvg:
    vg: rdb
    pvs: "{{ userctl_disks | map('regex_replace', '$', '1') | list }}"

# 論理ボリュームを作成する
- name: create logical volume
  lvol:
    vg: rdb
    lv: "{{ item.db_name }}"
    size: "{{ item.db_size }}"
  with_items: "{{ userctl_users }}"

# XFSでフォーマットする
- name: format for xfs
  filesystem:
    dev: "/dev/rdb/{{ item.db_name }}"
    fstype: xfs
    resizefs: yes
  with_items: "{{ userctl_users }}"

# マウントポイントを設定する
- name: mount point
  mount:
    src: "/dev/rdb/{{ item.db_name }}"
    path: "/var/lib/mysql/{{ item.db_name }}"
    fstype: xfs
    opts: 'defaults,discard'
    state: mounted
  with_items: "{{ userctl_users }}"

# マウントポイントの権限を変更する
- name: change owner and permission
  file:
    path: "/var/lib/mysql/{{ item.db_name }}"
    state: directory
    owner: mysql
    group: mysql
    mode: '0700'
  with_items: "{{ userctl_users }}"

- name: install pymysql
  pip:
    name: pymysql

# データベースを作成する
- name: mysql create db
  mysql_db:
    login_user: root
    name: "{{ item.db_name }}"
  with_items: "{{ userctl_users }}"

# データベースユーザを作成して、権限を指定する
- name: mysql create user
  mysql_user:
    login_user: root
    name: "{{ item.db_user }}"
    host: "{{ item.db_host | default('localhost') }}"
    password: "{{ item.db_pass }}"
    priv: "{{ item.db_name }}.*:{{ item.db_priv | default('ALL') }}"
    state: "{{ item.db_state | default('present') }}"
  with_items: "{{ userctl_users }}"

次に、実行するための変数を定義した以下のプレイブックを作成します。

main.yml
---
- hosts: localhost
  become: yes
  vars:
    # 物理ディスク
    userctl_disks:
    - /dev/sdb
    #- /dev/sdc
    #- /dev/sdd

    # ユーザ用データベース領域
    # - db_name: データベース名は使用できない文字が多いので注意
    #   db_host: 接続許可するIPアドレス(ホスト名)。省略すると 'localhost' になる
    #   db_user: ユーザ名は使用できない文字が多いので注意
    #   db_pass: パスワード
    #   db_size: '1G' とか '256M' とか
    #   db_priv: 'ALL' とか 'SELECT' とか。省略すると 'ALL' になる
    #   db_state: 'absent' を書くとアカウントが無効になる。※データは消えない
    userctl_users:
    - db_name: miku
      db_host: '%'
      db_user: 'hatsune-miku'
      db_pass: secret
      db_size: 32M
      db_priv: 'ALL'

  tasks:
  - include: "task.yml"

今後は、このプレイブックの変数に、追加・編集して ansible-playbook を実行するだけになったので、作業が簡単になりました。

注意事項

なお、LVMを使っての容量制限は。詳細まで検証しているわけではないです。最悪の場合データベースを破壊してしまうかもしれません。 この方法について、ご指摘がある方はコメントでお知らせください。

大量のデータをrsyncで転送できない問題とその解決策

問題

あるシステムで、大量のデータをrsync+ssh をデータを転送しようとするが、うまくできない問題が発生しました。 sshの接続性やrsyncの動作は問題ないことを確認済みで、データ量が多くなることに起因していると思われるので調査しました。

また、rsync の出力結果をみると sending incremental file list の1行だけが出力されていて、それ以降の出力がないためrsync処理中に中断しているような感じでした。

原因と解決策

原因は、単純で rsync のデータ転送の計算に時間がかかり過ぎたことにより、データ無転送の状態がしばらく続き ssh のコネクションが切れてしまったからでした。 そのため、サーバ側で ClientAliveInterval を指定するか クライアント側で ServerAliveInterval を指定することで解決ができます。 なお、サーバ側とクライアント側のどちらか片方の設定で十分です、両方にキープアライブ設定を入れても効果は変わりません。

サーバ側で指定する場合は、以下のように設定変更をします。

/etc/ssh/sshd_config
#ClientAliveInterval 0
ClientAliveInterval 60

クライアント側で指定する場合は、rsync の -e オプションで指定するのが簡単です。

$ rsync -e 'ssh -o ServerAliveInterval=60' ...

この手の問題は、普通に rsync する程度では発生せずデータが多くなることで発生するため、発生を予測してキープアライブ設定をする対応は難しいと思っています。 そのため、rsync に失敗しても気づけるような仕組みを導入したほうがいいでしょう。

静的サイトジェネレータをPelicanからHexoに切り替えた

背景

pelicanを採用した理由は python で書かれており、何か問題があってもトラブルシュートができるからでした。 実際、AsciiDocのプラグインが文字化けしたり正常に機能しないことが多々あり、自分の環境で動くように調整する必要がありました。

また、GitHub Pages のブロクを書いていて、コメントができないのは不便だなーと思いつつ、何も調べないまま Pelican を採用したのも失敗でした。 Pelican でも Disqus であれば、JS埋め込みで対応できるようですが、あまり標準で対応している感じではありませんでした。

そのため、Pelican をやめて Hexo に切り替えることにしました。

Hexo は nodejs で書かれており、npm でプラグインをインストールすることで様々な機能に対応することができます。 もちろん AsciiDoc にも対応したプラグインもあります。

僕は、以下のプラグインを入れています。

  • hexo-cli

  • hexo-renderer-asciidoc

  • hexo-plugin-gitalk

  • hexo-deployer-git

Hexo 環境整備

インストール

自分用のメモ
// Arch Linux の場合は pacman で npm をインストール
$ sudo pacman -S npm

// hexo-cli をインストール
$ sudo npm install hexo-cli -g

// サイトのプロジェクトフォルダを作成
$ hexo init harre-orz.github.io
$ cd harre-orz.github.io

// 依存パッケージをインストール
$ npm install

// AsciiDoc をインストール
$ npm install hexo-renderer-asciidoc --save

// gitalk コメントを使うためにをインストール
$ npm install hexo-plugin-gitalk --save

// Github Pages にデプロイするためのプラグインをインストール
$ npm install hexo-deployer-git --save

// git の初期化
$ git init
$ git remote add origin git@github.com:harre-orz/harre-orz.github.io.git

// deploy
$ hexo deploy -g

landscape の編集

標準で用意されているテーマ`landscape`の出来はいいのですが、AsciiDoc が生成するコードと相性が悪いようで、CSSの修正が必要でした。

themes/landscape/source/css/_partial/highlight.styl
link:themes/landscape/source/css/_partial/highlight.styl[]

asciidoc の編集

asciidoc もそのままでは セキュリティレベルが高く include が利用できません。レンダリングオプションに { safe: "safe" } を指定します。

node_modules/hexo-renderer-asciidoc/lib/renderer.js
link:node_modules/hexo-renderer-asciidoc/lib/renderer.js[]

gitalk の編集

gitalk も、標準の場合はトップページに先頭の記事の中にコメント枠が表示され、それがトップページ用のコメントとして認識されてしまいます。 そのため、記事ページに移動してからでないとコメントが表示されないように修正が必要でした。

node_modules/hexo-plugin-gitalk/index.js
link:node_modules/hexo-plugin-gitalk/index.js[]

結局、細かいところを直しつつというのは変わらない気がしますが。。。

CentOS7のrsyslogが大量にメモリを消費する問題

現在の CentOS7 最新版である rsyslog-8.24-0.34.el7 でメモリリーク問題が上がっています。 とあるLinuxサーバで、このバグを踏んで、同じように大量にメモリを消費して一時的にswap領域まで使用していました。

一時しのぎとして、rsyslog 公式リポジトリのバージョンを使うことで問題は解消されましたが、このままでは CentOS7 の rsyslog が使えないままなので、この問題を再現させる方法を調べました。

再現方法

単純に logger コマンドで /dev/log に書き出すのでは駄目で、長い文字列可能な限り速く 書き続ける必要がありました。

以下のC言語のコードで再現しました。

#include <syslog.h>

int main() {
  openlog("test", 0, LOG_USER);
  for(;;)
    syslog(LOG_ERR, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
}

早く修正されてほしい。

なぞのプロセス portreserve の調査

とあるLinuxサーバのプロセスを確認したところ portreserve というプロセスが 631 ポートを開いていました。 portreserve は今まで聞いたことのないサービス名だったので調査したところ「ポートを予約するためのサービス」であるとのこと。

CentOS7 でもパッケージが存在していたので、インストールしてみて man で確認したところ

/etc/portreserve/<ファイル名>
サービス名称( /etc/services に記載されている名前に限る )

を設定すれば、予約ができるとありました。

実験として、telnet のポートを予約する場合は、

/etc/portreserve/telnet
telnet

と記載して portreserve を再起動すれば portreserve が 23番ポートを bind しました。

nc コマンドで LISTEN しようとしてもエラーになります。

# nc -l 0.0.0.0 23
Ncat: bind to 0.0.0.0:23: Address already in use. QUITTING.

次に、このポートを解放する場合は、以下のように Datagram の UNIX-Domainソケットで /var/run/portreserve/socket に接続し、サービス名を送信します。

# echo -n "telnet" | nc -u -U /var/run/portreserve/socket

これで telnet で使用する 23番ポート は解放されるので、nc コマンドで LISTEN できるようになります。

# nc -l 0.0.0.0 23

以上が portreserve の使い方らしいのですが、使う場面が全くわかりませんでした。

docker+nextcloudで構築するときDBを初期化できないときの対処法

docker-compose で、nextcloud と postgresql を以下のように定義したが起動しなかった。

docker-compose.yml
version: '3'
services:
  app:
    image: nextcloud:15.0.5
    ports:
      - 8080:80
    depends_on:
      - db
    volumes:
      - nextcloud:/var/www/html
    environment:
      - POSTGRES_HOST=db
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=secret
      - NEXTCLOUD_ADMIN_USER=admin
      - NEXTCLOUD_ADMIN_PASSWORD=admin
    restart: always

  db:
    image: postgres:9.6
    restart: always
    volumes:
      - postgres:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=nextcloud
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=secret

volumes:
  nextcloud:
    driver: local
  postgres:
    driver: local

コンテナの出力には、以下のような permission denied for database となっているが、

db_1   | 2019-04-01 14:24:55.443 UTC [71] FATAL:  permission denied for database "nextcloud"
db_1   | 2019-04-01 14:24:55.443 UTC [71] DETAIL:  User does not have CONNECT privilege.

postgres のコンテナに nextcloud というデータベースで postgres ユーザがスーパーユーザでアクセスできているように見える。

$ docker-compose exec -it -u postgres db /bin/bash
$ psql nextcloud postgres
nextcloud=# \du
                                    List of roles
 Role name  |                         Attributes                         | Member of
------------+------------------------------------------------------------+-----------
 oc_admin   | Create DB                                                  | {}
 oc_admin10 | Create DB                                                  | {}
 oc_admin11 | Create DB                                                  | {}
 oc_admin12 | Create DB                                                  | {}
 oc_admin13 | Create DB                                                  | {}
 oc_admin14 | Create DB                                                  | {}
 oc_admin15 | Create DB                                                  | {}
 oc_admin16 | Create DB                                                  | {}
 oc_admin2  | Create DB                                                  | {}
 oc_admin3  | Create DB                                                  | {}
 oc_admin4  | Create DB                                                  | {}
 oc_admin5  | Create DB                                                  | {}
 oc_admin6  | Create DB                                                  | {}
 oc_admin7  | Create DB                                                  | {}
 oc_admin8  | Create DB                                                  | {}
 oc_admin9  | Create DB                                                  | {}
 postgres   | Superuser, Create role, Create DB, Replication, Bypass RLS | {}

nextcloud=# \q

このデータベースを削除することで nextcloud 側で自動的にデータベースを作成してくれた。

$ dropdb nextcloud postgres

nextcloud のコンテナの動作が今後変わると修正されるかもしれない。

journaldを経由せずにrsyslogから/dev/logを使う

背景

CentOS 7 で、とあるサーバを構築し、負荷試験を実施したところ systemd-journald がCPU100%使い切っており、本来のパフォーマンスが発揮されないことが判明しました。

レガシーの syslog() で秒間数万行以上の大量のログを出力しており、systemd-journald プロセスの処理が追いつかず /dev/log ソケットでメッセージが詰まってしまったことが原因でした。

根本的な原因は syslog() で大量のログを出力していることにあるのですが、systemd-journald も結局は rsyslog に転送してログファイルに書き出しているだけなので、ボトルネックになってほしくはありません。

systemd-journald が高負荷になった原因については、詳しくは調べていません。おそらくはメッセージの付属情報の取得やバイナリデータ用のデータベースへの書き込みなどの処理にあると思っています。

また、syslog() は、rsyslog でログファイルに書き込んでいます。そのため、systemd-journald のバイナリログと rsyslog のログファイルの両方に保存していることなるので systemd-journald を経由することは、はっきり言って無駄な処理となっています。

そのため、sytemd-journald を経由せずに直接 rsyslog で /dev/log を使う設定を調査しました。

設定

/dev/log は Datagram ベースの UNIX-domain socket です。

# fuser -v /dev/log
                     USER        PID ACCESS COMMAND
/dev/log
                     root          1 F.... systemd
                     root       6277 F.... systemd-journal

# netstat -x | grep dev
unix  3      [ ]         DGRAM                    103043   /dev/log

systemd-journald.socket ファイルの /dev/log をコメントアウトします。

/usr/lib/systemd/system/systemd-journald.socket
[Socket]
ListenStream=/run/systemd/journal/stdout
ListenDatagram=/run/systemd/journal/socket
#ListenDatagram=/dev/log  #コメントアウト

rsyslog の SystemLogSocketName を設定を変更します。

/etc/rsyslog.d/listen.conf
#$SystemLogSocketName /run/systemd/journal/syslog  #コメントアウト
$SystemLogSocketName /dev/log                      #デフォルト値なので記述しなくてもいいのですが、書いたほうが分かりやすいと思います

rsyslog の設定を変更します。

/etc/rsyslog.conf
$ModLoad imuxsock                    #アンコメント
#$ModLoad imjournal                  #コメントアウト
$OmitLocalLogging off                #off に変更
#$IMJournalStateFile imjournal.state #コメントアウト

#$imjournalRatelimitInterval 0       #記述した場合はコメントアウト
#$imjournalRatelimitBurst 0          #記述した場合はコメントアウト

systemd-journald と rsyslogd の再起動します。

# systemctl restart rsyslog
# systemctl status rsyslog
→status でエラーがないことを確認。

# systemctl restart systemd-journald
# systemctl status systemd-journald
→status でエラーがないことを確認。

/dev/log を掴んでいるプロセスを確認します。

# fuser -v /dev/log
                     USER        PID ACCESS COMMAND
/dev/log
                     root          1 F.... systemd
                     root       9218 F.... rsyslog
                     root       9234 F.... systemd-journal

systemd-jounral が /dev/log を掴んだままであれば、OS再起動をします。

# reboot

... 再起動後...

# fuser -v /dev/log
                     USER        PID ACCESS COMMAND
/dev/log
                     root          1 F.... systemd
                     root        128 F.... rsyslog

これで rsyslog で直接 /dev/log を取得できるようになりました。

sytemd-journald 自体のログ収集は止めていないため モダンな /run/systemd/journal/socket ソケットからのログは journalctl で検索可能です。

補足

この方法は、おそらく CentOS 7 でしか利用できません。別のディストビューションの場合、/dev/log は /run/systemd/journal/dev-log のシンボリックリンクになっている場合があります。

IPv6でGARPみたいなパケットを投げる

以下のソースコードでICMPv6のNeighbor Advertisement の override flag を設定し、強制的にIPv6アドレスとMACアドレスの対応関係を誘導させることが可能.

/*
 * Copyright (c) 1999-2017, Parallels International GmbH
 *
 * This code is public domain.
 *
 * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * Our contact details: Parallels International GmbH, Vordergasse 59, 8200
 * Schaffhausen, Switzerland.
 */

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>

#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <linux/types.h>
#include <linux/if.h>
#include <netinet/icmp6.h>

#include <sys/ioctl.h>
#include <arpa/inet.h>

#include <getopt.h>
#include <unistd.h>
#include <signal.h>

#define EXC_OK 0
#define EXC_USAGE	1
#define EXC_SYS		2
#define EXC_RECV	3
#define EXC_NORECV	4

char* iface = NULL;

struct in6_addr src_ipaddr;
int ifindex;
__u8 real_hwaddr[6];

struct nd_packet {
	__u8		icmp6_type;
	__u8		icmp6_code;
	__u16		icmp6_cksum;

	__u32		reserved:5,
			override:1,
			solicited:1,
			router:1,
			reserved2:24;
	struct in6_addr target;
	__u8		otype;
	__u8		ospace;
	__u8		obits[6];
};

int init_device_addresses(int sock, const char* device)
{
	struct ifreq ifr;

	memset(&ifr, 0, sizeof(ifr));
	strncpy(ifr.ifr_name, device, IFNAMSIZ-1);

	if (ioctl(sock, SIOCGIFINDEX, &ifr) != 0) {
		fprintf(stderr, "Unknown network interface %s: %m\n", device);
		return -1;
	}
	ifindex = ifr.ifr_ifindex;

	/* get interface HW address */
	if (ioctl(sock, SIOCGIFHWADDR, &ifr) != 0) {
		fprintf(stderr, "Cannot get the MAC address of "
				"network interface %s: %m\n", device);
		return -1;
	}
	memcpy(real_hwaddr, ifr.ifr_hwaddr.sa_data, 6);

	return 0;
}

int sock;
struct nd_packet pkt;

void create_nd_packet(struct nd_packet* pkt)
{
	pkt->icmp6_type = ND_NEIGHBOR_ADVERT;
	pkt->icmp6_code = 0;
	pkt->icmp6_cksum = 0;
	pkt->reserved = 0;
	pkt->override = 1;
	pkt->solicited = 0;
	pkt->router = 0;
	pkt->reserved2 = 0;
	memcpy(&pkt->target, &src_ipaddr, 16);
	pkt->otype = ND_OPT_TARGET_LINKADDR;
	pkt->ospace = 1;
	memcpy(&pkt->obits, real_hwaddr, 6);
}

void sender(void)
{
	struct sockaddr_in6 to;

	to.sin6_family = AF_INET6;
	to.sin6_port = 0;
	((__u32*)&to.sin6_addr)[0] = htonl(0xFF020000);
	((__u32*)&to.sin6_addr)[1] = 0;
	((__u32*)&to.sin6_addr)[2] = 0;
	((__u32*)&to.sin6_addr)[3] = htonl(0x1);
	to.sin6_scope_id = ifindex;

	if (sendto(sock, &pkt, sizeof(pkt), 0,
	    (struct sockaddr*) &to, sizeof(to)) < 0) {
		fprintf(stderr, "sendto function returned error: %m\n");
		exit(EXC_SYS);
	}
}

int main(int argc,char** argv)
{
	int value;

	if (inet_pton(AF_INET6, argv[1], &src_ipaddr) <= 0)
		return -1;
	iface = argv[2];

	sock = socket(PF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
	if (sock < 0) {
		fprintf(stderr, "socket function returned error: %m\n");
		exit(EXC_SYS);
	}

	if (init_device_addresses(sock, iface) < 0)
		exit(EXC_SYS);

	value = 255;
	setsockopt (sock, SOL_IPV6, IPV6_MULTICAST_HOPS,
		    &value, sizeof (value));

	create_nd_packet(&pkt);
	sender();
	exit(EXC_OK);
}