Neovimでどこからでもコードを書く/ノートを書く (ufw, Tailscale, Docker)

この記事は[DeNA 24 新卒 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/dena-24-newgrad)の 4 日目の記事です。

## TL;DR

- VPSを借りてどこからでもVimを触る.
- VPNサービス [**Tailscale**](https://tailscale.com/)でよりセキュアに.
- Dockerのpublishオプション`-p`は`ufw allow`, `ufw default deny routed`のフィルタリング設定に関わらずportを開ける
    - [**ufw-docker**](https://github.com/chaifeng/ufw-docker)の設定を使え.
- **OSC52**を使ってクリップボードをいい感じに.
- MarkdownのLSPやLinterを使うと尚良し.


## Motivation

下記のようなモチベーションがありました. 結果的に今回紹介する構成を組むに至りました.

- 個人のナレッジや個人開発の作業ログのノートを付ける場所がほしい
    - できるなら好きなエディタを使いたい.
    - あらゆる端末からアクセスしたい (Desktop, Laptop, Tablet, SmartPhone...)
    - Git & GitHubで管理してもいいが, commitやpushするほどでもない.
        - 様々な環境で別々に編集したのち, ノートのためにコンフリクト解決をしたくない.
    - できるだけ安く済ませたい.
    - 外出時など, 回線が切れても編集状況が残っていてほしい.
    - [HackMD](https://hackmd.io) など上記を満たすサービスはある.
        - しかし, いつでも素のエディタを使いたい.
        - なんならいつでもコードをかけるようにしたい.

## 現状の個人的な構成


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285541/cb8690ce-bfec-0f4e-8132-0f0e427cf10a.png)

以上のMotivationを達成するような下記のような環境を用意しました.
この記事も以下の環境で書かれています.

- IaaS: ConohaVPS
- OS: Ubuntu 22.04
- VPN: [Tailscale](https://tailscale.com/)
- Terminal Multiplexer: tmux
    - ClientでもServerでも使っています. tmux in tmux.
- Shell: [fish shell](https://fishshell.com/)
- Editor: Neovim
    - LSP on Markdown: [Marksman](https://github.com/artempyanykh/marksman)
    - Linter on Markdown: [markdownlint](https://github.com/markdownlint/markdownlint)
- Markdown Preview: Docusaurus

## サーバーの設定

以降は環境設定時のメモやTips, 注意点を述べていきます.

万一参考にする場合は, 自己責任&個人情報などの重要な情報は置かないようにしてください.
もし, 誤りがあれば躊躇なく指摘してください.

まずは基本的なセキュリティ対策を実施します.
具体的には基本的なことではありますが, 下記などを実施します

- userを作成 & sshでrootログインさせないように
- Password認証ではなく公開鍵認証を使う
- 不必要なポートを開放しない
    - 今回はufwを使いました.

## Tailscale: VPNサービス

/var/log/auth.log には様々なログインの試みが見られます.

加えて開発用のサーバーを立てることなどを見据えて,
よりセキュアにしたいと考え, WireGuardプロトコルを利用したP2PのVPNサービスであるところの**Tailscale**を導入しました.

次の記事に沿ってTailscale経由でのアクセスのみを許可する設定を導入しました.

https://tailscale.com/kb/1077/secure-server-ubuntu-18-04/

しかし, SSH接続に関しては, Tailscale SSHなるものがあるようで, これは

- 鍵管理をしなくて良い
- ブラウザのAdmin Consoleから ターミナルを開けるなどの利点があり便利です.

Tailscale SSH 参考記事:

https://zenn.dev/watany/articles/088cdee8441511

## ufw使用時 Dockerのpublishオプション`-p` に注意

サーバー上でDockerを使った開発をしたくなる場合があります.
ですが, ufwでfirewallを作成している場合には注意すべき点があります.

Tailscale経由でアクセスできるDocker ContainerをVPSに建てることを考えます.

### ❌ 状況: **悪い例**

- `ufw allow in on tailscale0 to any port 3000`
- Dockerのpublishオプション `-p 3000:3000` で起動したコンテナ

このように設定すると, たしかにTailscaleクライアントからVPSの`<VPNのip>:3000`に
接続できるのですが, Tailscaleに接続していなくても:3000にアクセスできてしまい危険です.

### 原因

これの原因は, `iptables` を確認するとよくわかります.

iptablesの概観はこちらの記事が参考になりました.

https://christina04.hatenablog.com/entry/iptables-outline

![ufw-docker.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/285541/2a4235e6-e54f-16cb-7c0e-3a39c5a5f04e.png)

図では太矢印が:3000へのパケットの流れを示します.

まず, パケットはiptablesのnatテーブルの**PREROUTING**チェインに来ます.

**PREROUTING**チェインには, Dockerのpublishオプション `-p 3000:3000`によって
サーバーの:3000へのパケットをDocker ContainerのnetworkにFORWARDする処理が追加されています.
この結果, パケットはその後**FORWARD**チェインによって処理されることになります.

一方, `ufw allow` はサーバーのローカルホスト内(≠Docker Container内)で処理される**INPUT**チェインに対してのフィルタリングを設定しています.

そのため, この設定内容は**FORWARD**チェインで処理されるDocker Containerへのパケットには適用されません.

加えて, ufwで**FORWARD**チェインにフィルタリングを追加する `ufw route` コマンドや `ufw default deny routed` を設定しても機能しません.

iptablesを確認すると, ufwの**FORWARD**をフィルタリングするチェーンの前にDocker Containerへのパケットを**ACCEPT**する処理が設定されるようです.

#### 参考: iptablesを確認してみる

natテーブルを見る.

```bash
sudo iptables -t nat -S
```

結果:

```txt
...(略)...

# パケットの宛先がローカルマシン宛ならDockerチェインに飛ばす
-A PREROUTING -m addrtype --dst-type LOCAL -j Docker

...(略)...

# Dockerチェイン: Dockerの特定のbridgeから来ていないあらゆるtcpポート3000宛のパケットをコンテナにフォワーディングする.
-A Docker ! -i br-************ -p tcp -m tcp --dport 3000 -j DNAT --to-destination 172.18.0.2:3000

...(略)...
```

filterテーブルを見る.

```bash
sudo iptables -t filter -S
```

結果:

```txt
...(略)...
# ufwのチェインの前にDOCKERチェインに飛ぶ.
-A FORWARD -o br-************ -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o br-************ -j DOCKER
-A FORWARD -i br-************ ! -o br-************ -j ACCEPT
-A FORWARD -i br-************ -o br-************ -j ACCEPT
-A FORWARD -j ufw-before-logging-forward
-A FORWARD -j ufw-before-forward
-A FORWARD -j ufw-after-forward
-A FORWARD -j ufw-after-logging-forward
-A FORWARD -j ufw-reject-forward
-A FORWARD -j ufw-track-forward

...(略)...
# DOCKERチェイン内で条件を満たすとACCEPTされる.
-A DOCKER -d 192.168.*.*/32 ! -i br-79eeb1c1c541 -o br-79eeb1c1c541 -p tcp -m tcp --dport 3000 -j ACCEPT
...(略)...
```

### 解決策

#### 🤔 `-p` オプションのホストをちゃんと設定しておく

対処方法として, `-p <VPNのip>:3000:3000` などとホストを設定することがまず考えられますが, 設定を忘れてしまう可能性があります.

`/etc/docker/daemon.json` のipプロパティでdefaultのホストを`<VPNのip>`に設定して設定忘れを防止することも考えられます. 

#### 👍 ufw-docker

上記の課題を解決してくれるレポジトリがありました.

https://github.com/chaifeng/ufw-docker

FORWARDチェイン内のDOCKERチェイン以前に実行されるDOCKER-USERチェインに設定を追加します.

DOCKER-USERチェインから, `ufw route allow` コマンドで設定されたフィルタリングを行うufw-user-forwardチェインに
飛ばすようにします.

これによって, `ufw route allow` コマンドでDocker ContainerのネットワークへのPort Forwardをフィルタリングしてくれます.

ufw-Dockerの設定を正しく適用できたのち, 以下のように設定すればTailscale経由でDocker Containerに安全にアクセスできるはずです.[^1]

```bash
sudo ufw route allow from 100.64.0.0/10  to any port 3000
```

最初からiptablesのみを使ってFilteringをしろという話はあると思います.
iptablesのいい勉強になりました.

## Terminal & Editorの設定

先述の通りTmux x Neovimを愛用しています.
拙いですがせっかくなのでdotfilesを置いておきます.

https://github.com/wbelucky/dotfiles

### OSC52

ssh先の文字列をコピーする際,
クライアント端末の画面が横に短い場合, URLなどの長い文字列を一度にコピーすることができません.
技術メモを書く際, 多くのURLを貼る, 辿る必要あるため, ssh先のNeovimのクリップボードを簡単に取得できるようにしたいです.

そこでosc52対応ターミナル & neovimのosc52のpluginを使うのが便利です.
osc52はXTermのクリップボード制御シーケンスで, クライアント側のターミナルがシーケンスを処理できるものであれば, 利用することができます.

https://github.com/ojroques/nvim-osc52

tmuxもOSC52に対応しており, tmux in tmuxの際の設定はこちらが参考になります.

https://github.com/tmux/tmux/wiki/Clipboard#terminal-support---tmux-inside-tmux

### LSP

Markdownでメモをとるのですが, 往々にして自分の書いたMarkdownのHeadingへのリンクを貼ります.
そのリンクの位置に即座にジャンプしたいという願いを叶えてくれるLSPがMarksmanです.

https://github.com/artempyanykh/marksman

`[inline link](/some-file.md#some-heading)` などの形式のリンクに対してGoToDefinitionのショートカットでそのHeadingの位置に飛ぶことができます.

また, Markdownファイル名の補完やHeadingの補完も行ってくれるため便利です.

## まとめ

理想のリモート個人開発環境&メモ環境を作ることができました.
それぞれ詳細は省いていますが, メモ環境構築の取っ掛かりとしてお役に立てれば幸いです.

明日は [nntto](https://qiita.com/nntto) さんの記事です! お楽しみに.

[^1]: on tailscale0などネットワークカードを指定するオプションがありましたがうまく使えませんでした. ip範囲で設定しています. 100.64.0.0/10を設定する方法は[こちらのissue](https://github.com/chaifeng/ufw-docker/issues/47)参照のこと.