squid を使ってみた

こんにちは masm11 です。

systemd シリーズの途中ですが、ここで別のネタを挟みたいと思います。 タイトルのとおり、今回は squid です。

個人的に squid をインストールして使ってみましたので、説明してみたいと思います。

squid とは / 使用目的

squid は、プロキシサーバです。http だけでなく https にも ftp にも、知る人ぞ知る gopher にだって対応しています。

個人的に使うだけなので、簡単なものが良かったのですが、ほぼデファクトスタンダードの ようなので、これを使ってみました。

Web サービスを開発・運営していると、「時々おかしい!」というメッセージとともに ブラウザのスクリーンショットが送られてきて、 そこに "squid" という文字列が見えることが、時々あります。 そういったことからも「デファクトスタンダードなんだなぁ」と感じます。

さて、私は Linux マシンを 3台持っています。全て ArchLinux です。 ArchLinux はローリングアップデートを採用しています。 「OS のバージョンというものがなく、パッケージがそれぞれのタイミングで更新されていく」 というと解りやすいでしょうか。 例えば、一昨日は GNOME が更新され、昨日は GCC が更新され、今日は binutils が、 といった感じです。

わりと頻繁にアップデートしていても、ダウンロードサイズが 100MB を超えることは普通にありますし、 しばらく放置していて久しぶりにアップデートしようと思ったらダウンロードサイズが 1GB 超だった! なんてこともあります。

こんなマシンを 3台持っていると、「ギガが減る」わけです。せめて、3台を同じタイミングで 更新する時くらいは、パッケージファイルをキャッシュしたいな、と思うわけです。

そこで squid を使ってみることにしました。

インストールする

Linux 系 OS なら、おそらくパッケージが用意されていると思います。 私は ArchLinux なので、

sudo pacman -S squid

でインストールできました。

設定する

設定ファイルは /etc/squid/squid.conf にあります。 このファイルは最小構成だそうで、ドキュメントは /etc/squid/squid.conf.documented にあります。 (ArchLinux 以外でも同様なのかはわかりませんが)

acl 設定

上から順に見ていきます。

acl localnet src 0.0.0.1-0.255.255.255  # RFC 1122 "this" network (LAN)
acl localnet src 10.0.0.0/8             # RFC 1918 local private network (LAN)
acl localnet src 100.64.0.0/10          # RFC 6598 shared address space (CGN)
acl localnet src 169.254.0.0/16         # RFC 3927 link-local (directly plugged) machines
acl localnet src 172.16.0.0/12          # RFC 1918 local private network (LAN)
acl localnet src 192.168.0.0/16         # RFC 1918 local private network (LAN)
acl localnet src fc00::/7               # RFC 4193 local private network range
acl localnet src fe80::/10              # RFC 4291 link-local (directly plugged) machines

ここでは、「localnet とは」を定義しています。 設定は十分なように見えて、実は自分が使っている IPv6 アドレスは当然含まれていませんので、 追加しておきます。

acl localnet src 2001::/16

私のネットワークの IPv6 アドレスは頻繁に変わるため、広く設定しました。 ファイアウォールで外部からのアクセスはありませんので、これでも大丈夫だと思います。

acl SSL_ports port 443

次に、「SSL_ports とは」を定義しています。SSL ポートは 443、つまり HTTPS ですね。

acl Safe_ports port 80          # http
acl Safe_ports port 443         # https

これは「Safe_ports とは」を定義しています。これは Web サーバのポート番号で、80 と 443 です。 他にもデフォルトで ftp や gopher やその他たくさん設定がありましたが、コメントアウトしました。

acl CONNECT method CONNECT

これは「CONNECT とは」を定義しています。ブラウザ-プロキシ間の通信に使う CONNECT メソッドである、 という設定です。

アクセスコントロール設定

さて、ここまでは、「localnet とは」「SSL_ports とは」といった定義をしてきました。 ここからは、これらを使ったアクセス制御を設定していきます。

http_access deny !Safe_ports

「Safe_ports でなかったら」deny、つまり即エラーを返します。

http_access deny CONNECT !SSL_ports

こちらは条件を2つ書いています。「CONNECT メソッドである」かつ「SSL_ports でない」場合です。 CONNECT なのに接続先が 443 でなかったらエラーとしています。

http_access allow localnet
http_access allow localhost

localnet (先程定義したものです) や localhost は許可しています。

http_access deny all

ここまでのいずれにも該当しなければ、エラーとしています。

プロキシ受け付けポートの設定

次にプロキシ受け付けポートの設定をします。

http_port 8443 ssl-bump generate-host-certificates=on dynamic_cert_mem_cache_size=4MB tls-cert=/etc/squid/pki/ica_crt.pem tls-key=/etc/squid/pki/ica_key.pem
sslcrtd_program /usr/lib64/squid/security_file_certgen -s /var/local/squid/ssl_db -M 4MB
sslcrtd_children 5
ssl_bump bump all
sslproxy_cert_error allow all

通常、HTTPS のプロキシは、トンネルをするだけです。プロキシには暗号化されたデータが通るので、 プロキシは通信内容を知ることができません。それでは ArchLinux のパッケージファイルをキャッシュ することができません。通信内容を知る必要があります。

squid の世界では ssl-bump と呼ぶようです。squid がブラウザと TLS 通信し、サーバとも TLS 通信します。こうすれば通信内容を知ることができます。

ただし、ここには問題があります。ブラウザは www.google.com と通信しようとしているのに、 サーバ証明書が masm11.me ではブラウザが拒否します。 かといって、www.google.com の証明書を (あくまで合法的に) もらってきても、秘密鍵がないため、 TLS 通信することができません。 そこで、これを解決するために、squid が www.google.com の秘密鍵とオレオレ証明書を自動生成します。 もちろん、これだけではブラウザが拒否しますので、オレオレルート CA 証明書を ブラウザに登録して信用させておく必要があります。

この辺りの事情を設定したのが、上記設定内容の1行目になります。 ブラウザから受け付けるポート番号は 8443 で、ssl-bump モードで、 サーバ証明書は自動生成し、 証明書はいくつかメモリにキャッシュしておき、 中間 CA 証明書と秘密鍵を設定しています。

ここに設定した中間 CA 証明書と秘密鍵は、今回自分で作成しましたが、作成手順を書こうとすると、 それだけで記事1本書けてしまいますので、ここでは割愛します。 以下のページを参考に作成しました。

https://qiita.com/bashaway/items/ac5ece9618a613f37ce5

生成されたサーバ証明書は、ディスクに持っておき、再利用します。 格納先が2行目にある /var/local/squid/ssl_db で、 /usr/lib64/squid/security_file_certgen というプログラムが証明書を生成して 格納するようです。

なお、/var/local/squid/ssl_db というディレクトリは初期化が必要です。

/usr/lib64/squid/security_file_certgen -c -s /var/local/squid/ssl_db

を実行することで、初期化することができます。

3行目は、このプログラムを5つまで起動するのでしょう。

4行目、

ssl_bump bump all

については、1行目で ssl-bump モードにはしたものの、全てそれでいいかと言うと、 そうでない場合もあり得ます。例えば接続元 IP アドレスによっては ssl-bump したくない、 ということもありそうです。

上記では、全て bump する設定にしてあります。

全く未確認の設定で恐縮ですが、おそらく、

ssl_bump bump localnet
ssl_bump splice all

と設定すれば、localnet からの接続なら bump し、それ以外は splice (つまりトンネリング) する ものと思われます。

キャッシュ格納先の設定

次にキャッシュを格納するディレクトリについての設定です。

cache_dir ufs /var/cache/squid 4096 16 256

/var/cache/squid に置きます。4096 は MB 単位で、4GBになります。 あとの数値2つは、キャッシュディレクトリの構造を指定するもので、 だいたい固定のようです。

キャッシュの有効期間の設定

最後に、キャッシュの有効期間についての設定です。

refresh_pattern ^ftp:        1440    20%     10080
  • /^ftp:/ の場合は
  • 最小キャッシュ時間 1440分 (=1日)
  • 鮮度しきい値 20%
  • 最大キャッシュ時間 10080分 (=1週間)

という設定です。

最小キャッシュ時間は、キャッシュに入れてからこの時間が経っていない場合、キャッシュから返します。 最大キャッシュ時間は、キャッシュに入れてからこの時間が経っている場合、改めてサーバから取得して返します。

また、鮮度という概念があります。鮮度は以下の式で計算されます。

(そのファイルがキャッシュに入ってから今までの時間) / (そのファイルがサーバに作成されてから今までの時間)

これが 20% 未満だったら、キャッシュから返し、20% 以上だったらサーバから取得して返します。

具体的にどういう感じなのか、よく解らないですね。

(now - cache_create) / (now - server_create) = 0.2

式変形すると、以下のようになります。

(now - cache_create) = (now - server_create) / 5

キャッシュの age がサーバ上の age の 1/5 になったら expire です。

いまいち意味がつかみづらいので、もう少し変形してみると、以下のようになります。

(now - cache_create) = (cache_create - server_create) / 4

キャッシュ作成時におけるサーバファイルの age の 1/4 が経過したら expire、ですね。

ただし、「最小キャッシュ時間」「最大キャッシュ時間」のパラメータもありますし、 HTTP ヘッダでの expire 設定もあり、これらの方が優先度は上です。

頻繁に更新されるページはいつまでも持ってるわけにはいかないけど、 長いこと放置されてるページは少々長く持ってても大丈夫、という考えで、 このパラメータを使うと、そういう設定ができるのでしょう。

しかし、ちょっと複雑すぎますね。

結局私は、もともとの「パッケージファイルをしばらくキャッシュしたい」 という目的に立ち返り、以下のように設定しました。

refresh_pattern \.pkg\.tar\.zst$        0       100%    4320
refresh_pattern .                       0       0%      0

.pkg.tar.zst は、パッケージファイルの末尾です。その場合は最大3日間キャッシュします。 それ以外の場合は、キャッシュしません。

使ってみる

まず、squid を起動します。

sudo systemctl start squid

ちゃんと起動したか確認します。

● squid.service - Squid Web Proxy Server
     Loaded: loaded (/usr/lib/systemd/system/squid.service; enabled; vendor preset: disabled)
     Active: active (running) since Sat 2020-11-07 01:32:28 JST; 1min 16s ago
       Docs: man:squid(8)
   Main PID: 831014 (squid)
      Tasks: 9 (limit: 18973)
     Memory: 24.2M
     CGroup: /system.slice/squid.service
             ├─831014 /usr/sbin/squid --foreground -sYC
             ├─831024 (squid-1) --kid squid-1 --foreground -sYC
             ├─831030 (security_file_certgen) -s /var/local/squid/ssl_db -M 4MB
             ├─831031 (security_file_certgen) -s /var/local/squid/ssl_db -M 4MB
             ├─831032 (security_file_certgen) -s /var/local/squid/ssl_db -M 4MB
             ├─831033 (security_file_certgen) -s /var/local/squid/ssl_db -M 4MB
             ├─831034 (security_file_certgen) -s /var/local/squid/ssl_db -M 4MB
             ├─831046 (logfile-daemon) /var/log/squid/access.log
             └─831047 (unlinkd)

1107 01:32:28 mike squid[831024]:         0 Objects cancelled.
1107 01:32:28 mike squid[831024]:         0 Duplicate URLs purged.
1107 01:32:28 mike squid[831024]:         0 Swapfile clashes avoided.
1107 01:32:28 mike squid[831024]:   Took 0.06 seconds (5449.45 objects/sec).
1107 01:32:28 mike squid[831024]: Beginning Validation Procedure
1107 01:32:28 mike squid[831024]:   Completed Validation Procedure
1107 01:32:28 mike squid[831024]:   Validated 319 Entries
1107 01:32:28 mike squid[831024]:   store_swap_size = 230576.00 KB
1107 01:32:29 mike squid[831024]: storeLateRelease: released 0 objects
1107 01:32:38 mike systemd[1]: /usr/lib/systemd/system/squid.service:15: PIDFile= references a path below legacy directory /var/run/, ...

大丈夫そうです。次の OS 再起動時にも自動起動するように設定しておきます。

sudo systemctl enable squid

wget を使ってプロキシ経由の通信を試みてみます。

luna:~ % http_proxy=mike.local:843 \
 https_proxy=mike.local:8443 \
 /usr/bin/wget --ca-certificate=/etc/pacman.d/RootCA_crt.pem \
 https://www.google.com/
--2020-11-07 01:37:03--  https://www.google.com/
CA証明書 '/etc/pacman.d/RootCA_crt.pem' をロードしました
mike.local (mike.local) をDNSに問いあわせています... IPv6アドレス, IPv6アドレス
mike.local (mike.local)|IPv6アドレス|:8443 に接続しています... 接続しました。
Proxy による接続要求を送信しました、応答を待っています... 200 OK
長さ: 特定できません [text/html]
`index.html.1' に保存中

index.html.1                         [ <=>                                                       ]  13.74K  --.-KB/s 時間 0.01s    

2020-11-07 01:37:04 (1.34 MB/s) - `index.html.1' へ保存終了 [14068]

luna:~ % 

なんだかうまくいってそうです。

なお、ご存知かもしれませんが、プロキシは、HTTP と HTTPS を同じポートで待ち受けることができます。 最初に生で CONNECT が送られて来たら、そこから TLS handshake が始まります。 最初に生で GET 等が送られて来たら、それは生の HTTP です。 簡単に区別できるんですね。

では、ArchLinux のパッケージマネージャ pacman に設定します。

/etc/pacman.d/RootCA_crt.pem にオレオレルート CA 証明書を置いています。 その上で、/etc/pacman.conf には以下のように書きます。

XferCommand = /usr/bin/env https_proxy=mike.local:8443 http_proxy=mike.local:8443 /usr/bin/wget --ca-certificate=/etc/pacman.d/RootCA_crt.pem -c -O %o %u

これで

sudo pacman -Syu

を実行すると、更新されたパッケージがプロキシ経由でダウンロードされてきます。

squid の通信ログは /var/log/squid/access.log にあります。その中から引用してみます。

1604669997.994      2 クライアントIPv6アドレス NONE/200 0 CONNECT ftp.jaist.ac.jp:443 - HIER_NONE/- -
1604669998.326    303 クライアントIPv6アドレス TCP_MISS/200 635208 GET https://ftp.jaist.ac.jp/pub/Linux/ArchLinux/community/os/x86_64/iwd-1.9-1-x86_64.pkg.tar.zst - HIER_DIRECT/2001:df0:2ed:feed::feed application/x-tar

1行目は CONNECT の処理ですね。NONE/200 の 200 の部分が、クライアントに http 200 を返したことを示しています。 2行目はパッケージの GET の処理です。キャッシュになかった (TCP_MISS) ので、直接ダウンロードした (HIER_DIRECT) ことがわかります。

もう一箇所引用します。

1604669997.505      3 クライアントIPv6アドレス NONE/200 0 CONNECT ftp.jaist.ac.jp:443 - HIER_NONE/- -
1604669997.592     68 クライアントIPv6アドレス TCP_HIT/200 1378946 GET https://ftp.jaist.ac.jp/pub/Linux/ArchLinux/extra/os/x86_64/libxml2-2.9.10-3-x86_64.pkg.tar.zst - HIER_NONE/- application/x-tar

1行目は先程と同様です。 2行目はキャッシュにあった (TCP_HIT) ため、ダウンロードしなかった (HIER_NONE) ことがわかります。

なお、TCP_MISS, TCP_HIT などと、「TCP」という語が見えますが、TCP/IP の「TCP」とは全く無関係のようです。

まとめ

以上、squid の導入から pacman を使った確認まで、一気にご説明しました。

ふ~疲れました。

しかし、デファクトスタンダードたる squid の底力はこんなものではないはずです。 せっかくインストールしてみたので、この機にいろいろ試してみたいと思います。 特に websocket がどうなってるのか、とか。

ではまた!!