プログラマ行進曲第二章

主にソフトウェア関連の技術をネタにした記事を執筆するためのブログ

必要なサイトのみVPN経由でアクセスするOpenVPNの設定ファイルを生成する

VPN経由で作業をしないといけないケースがあります。

この時のクライアントソフトとしてよく使われているのがOpenVPN、またはOpenVPNベースのVPNクライアントです。AWS Client VPNとかそうですね。

こういうVPNを使うケースとしては、ある特定のサイトを不特定多数に公開しないようにすることがメインの目的として使われることが多いでしょう。

こういった場合、その特定のサイト以外は別にVPN経由でアクセスする必要は無いわけです。全ての通信をVPN経由にすると、よく分からない理由でネット接続の速度が落ちたりするといった経験もあり、必要なサイトにのみVPN経由でのアクセスをしたいと思っていました。

そんなことをしようと思って色々調べてやったことをまとめようと思います。

盛大に間違っているところもありそう*1なので、そんな箇所があったら誰か優しく指摘してください。

まず手元の設定ファイルの中身を見てみる

手元のOpenVPN設定ファイルの中を見てみるとこんな感じでした。隠さないといけないものは適宜値を変えて記載します。

client
dev tun
proto udp
remote clientvpn.example.com 443
remote-random-hostname
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-GCM
verb 3

<ca>
-----BEGIN CERTIFICATE-----
<sensitive>
-----END CERTIFICATE-----

</ca>
<cert>
-----BEGIN CERTIFICATE-----
<sensitive>
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
<sensitive>
-----END PRIVATE KEY-----
</key>
auth-user-pass

reneg-sec 0

この記述だけだとVPN接続をオンにした際、全てのネット接続がVPN経由になってしまいます。なので、ここの設定を変えて、必要なサイトのみVPN経由でアクセスするようにしたいのです。

結論から言うと、既に記載済みの要素を変える必要はなく、ほんの少し設定を追加すればいいだけでした。

ただ、応用を利かせるようにするにはそれぞれの設定項目の意味を知った方がいいでしょう。クラスメソッドさんの以下の記事など参考になるかと思います。

dev.classmethod.jp

OpenVPNのリファレンスを見て必要な要素を確認

2024/01/08時点での最新のOpenVPNのリファレンスを見てみましょう。

openvpn.net

この中で必要になる要素は以下の2つです

  • route-nopull
  • route

それぞれリファレンスの中身を引用します。

route-nopull は以下の通り。

When used with --client or --pull, accept options pushed by server EXCEPT for routes, block-outside-dns and dhcp options like DNS servers. When used on the client, this option effectively bars the server from adding routes to the client's routing table, however note that this option still allows the server to set the TCP/IP properties of the client's TUN/TAP interface.

route は以下の通り。

Add route to routing table after connection is established. Multiple routes can be specified. Routes will be automatically torn down in reverse order prior to TUN/TAP device close.

Valid syntaxes:

route network/IP
route network/IP netmask
route network/IP netmask gateway
route network/IP netmask gateway metric

This option is intended as a convenience proxy for the route(8) shell command, while at the same time providing portable semantics across OpenVPN's platform space.

要するにこの2つのオプションを活用して、自分の設定ファイルに書かれたrouteのみ追加するように変えればいいわけです。

前述のクラスメソッドさんの記事曰く、以下のリポジトリのファイルに主な設定の書き方があるそうなので、そちらを見ると他のケースで参考になるかもしれません。

github.com

Goを使って設定ファイルを生成する

上記のrouteですが、困ったことにこれはドメイン名単位では書けないようで、IPアドレス単位でそれぞれ書くもののようです。

多くの場合、VPN経由でアクセスしたい先というのはドメイン名単位での指定であって、IPアドレス単位での指定ではないと思います。そういった場合は名前解決をしてIPアドレスを特定すればいいのですが、ものによってはVPN接続する前に都度名前解決してやらないといけない、ということもあります。

例えば、このアクセスしたい先がドメイン名で示されていて、その指し示す先がAWSのApplication Load Balancerだった場合、名前解決した後に出てくるIPアドレスは可変のため*2、一回IPアドレスを突き止めたからはい終わり、というわけにはいかないこともあるでしょう。

対象が2, 3のみならその都度dig等でIPアドレスを調べて設定ファイルを書くというやり方でもいいかもしれませんが、現実には対象は10を越えるため、その都度コマンドを実行するのは非常に骨が折れます。

なので、ここはちょっとしたプログラムを書いて設定ファイルを生成することにしました。

シェルスクリプト達人ならシェルスクリプトを駆使して書けるのかもしれませんが、私はそういったスキルは持ち合わせていないので、ここではGoを使ってOpenVPNの設定ファイルを生成することにしました。

ここでGoを選んだのは次の理由からです。

  • text/templateというテンプレート生成のために使えるパッケージが標準で付属していること
  • クロスコンパイルが楽なため、可搬性があること

慣れないプログラミングに四苦八苦しながら書いてそれなりに(?)まともになったかな、と思うようになったプログラムが以下のコードです。

sample codes for generating OpenVPN config file

普段Goどころかプログラムをバリバリ書いているわけではないため、これだけ書くのだけでも無駄に時間がかかってしまいましたが、一応週の初めにこのコマンドで生成した設定ファイルを読み込ませればだいたい困らないようになったので、取りあえずの目的は達成したということで自己満足しています。

上のプログラムでやっていることのポイントをまとめると以下になります。

  1. VPN経由でアクセスしたいドメイン名をリストしているdomains.jsoncにはJWCC(JSON With Commas and Commentsという記法を使っている。
    • 単なるJSONだとコメントが書けず、ドメイン名だけだと何のためのものか分かりづらい場合*3に管理が煩雑になるため。
    • JSON内にコメントを書きたいだけなので、tailscale/hujsonというパッケージを使って楽にやっている。
  2. そんなに複雑なことをしない想定だったので、引数の処理は標準のflagパッケージを使用している。
  3. テンプレートファイルのprofile.ovpn.tmpl内でIPアドレスの記述をするところの終わりの所を{{- end }}と、{{-を使うことで空行を抑制し、生成語のファイルの設定を見やすくしている。

あと、Goを全く書き慣れてないのでerrgroupの使い方が盛大に間違っているかもしれないな、って思いながらこの記事を書いています。

世の中にはもっとスマートな方法があるかもしれません。もしそういう方法をご存じの方は私にこっそり教えてください。

いつの間にかはてなブログにグループ貼り付けという機能が追加されていたので試してみました。ランキングとやらに反映されるそうなので、クリックしてやってもいいって人はクリックしてください。

*1:特にOpenVPNやネットワークの理解が間違っているのが容易に想像できる

*2:おそらくオートスケーリングする都合で内部で使われているEC2がその都度変わるためにそうなっているのだと予想します

*3:たとえばEKSのAPIエンドポイントなどが該当する