Linux/squidのインストール(リバースプロキシ)

squid でリバースプロキシ(2017-09-09、更新:2018-09-23)


余談:
squidはバージョンがどんどん上がって、パラメタ変更も激しい気がするので
このメモの賞味期限切れは早いと思います。

さて、
squid はプロキシ-キャッシュサーバで、
通信の間に入って、通信内容に応じて転送したり止めたりする機能やら
保持した情報を本体の代わりに応答することで、本体の負荷を減らすやら
色々できます。

今回は squid でWebサーバのリバースプロキシにしてみます。

Webブラウザ --- Squid --- Webサーバ

のように、通信の間でSquidが転送 or 切断してあげるイメージです。
Squidで不正アクセスに 404 応答あるいは即切断*1を実行します。
将来的には、
https を Squid で平文に戻すことで snort など IPS が通信を読みやすくしたり、
一部の通信はWebサーバ以外に転送させることも視野に。
(むかし導入したWAFはSquidの転送機能を利用していました)
追々色々やってみたいです。

ソースの取得とビルド

squid-cache.org (英語)で
ダウンロードから設定例まで探せます。
(同サイトから 日本語訳しているリンク(j-one様)にも行けます)

まず、ダウンロードページから、
ソースと、 sig ファイルを拾ってきてから、ハッシュ値を確認します。

# cat squid-3.5.24.tar.gz.asc
(確認方法について情報を得る。今回は「SHA1」行を参照してみます)
# sha1sum squid-3.5.24.tar.gz
(上記、ascファイルの中の SHA1 行と一致すること)

あとは、INSTALL テキストの指示通りです。

# tar zxvf squid-3.5.24.tar.gz
# cd squid-3.5.24
# ./configure --prefix=/usr/local/squid
# make
# make install

configure の オプションは --help を参照。
etc や var の置き場を /usr/local/squid 以外へにするなら configure する時に指定しましょう。


ですが、私の場合は、OpenSSLをソースからビルドしているので念のために。

# ./configure --prefix=/usr/local/squid  --with-openssl=OpenSSL関連のPATH LDFLAGS=-LライブラリのPATH
(中略)
checking for SSL_library_init in -lssl... no

OpenSSL 1.1.0 には 3.5.24版時点では未対応のようでした。
4.2版では、make は通りました。
(ChangeLogには4.0.18版で「Bug 4599: support OpenSSL 1.1」ってありますが、
 それ以外にWebで安心できる記述を見つけられなかったので、私は使いながらしばらく様子を見てみる感じです。)

参考まで、
OpenSSLの1.1 と 1.0 を同じ環境で使い分けるための考察はこちら「Linux/ビルドとyumの混在


無事、インストール出来たら、試しに(初期値のまま)起動。

# /usr/local/squid/sbin/squid -z
WARNING: Cannot write log file: /usr/local/squid/var/logs/cache.log
/usr/local/squid/var/logs/cache.log: Permission denied

ぬ!?
デフォルトだと、logfile-daemon がnobady起動でビルドされたためです。

# chown -R nobody:nobody /usr/local/squid/var/logs
(あるいは起動ユーザを nobody 以外に変える)
# /usr/local/squid/sbin/squid -z
# /usr/local/squid/sbin/squid
# ps -ef | grep squid

とりあえず、起動するところまで行きました。
一旦「/usr/local/squid/sbin/squid -k kill」か kill コマンドで
squid を停止して、コンフィグを設定します。

squid.conf の設定(リバースプロキシ)

今回は前述の通り 3.5版ですが、*2
squid もバージョンで結構パラメータが変わっている印象があって、
むかし(2.x版?)にsquid を構築した時と使用するパラメータが違う気がしました。

www.squid-cache.org から最新版に対応した設定例が探せます。リバプロが欲しいので、
[Examples] - [Reverse Proxy (Acceleration)] - [BasicAccelerator] と探しました。

以下、squid.conf への追記内容。
(たぶん、バージョンに依存するので参考程度に)

http_port 80 accel defaultsite=localhost
https_port 443 accel cert=サーバ証明書 key=証明書の秘密鍵 defaultsite=localhost 
cache_peer localhost  parent 8080 0 no-query originserver login=PASS name=my_accel

acl web_client dstdomain null-i.net www.null-i.net subdomain.null-i.net
http_access allow web_client
cache_peer_access my_accel allow web_client
http_access deny all

前半、
まず、前提として、
デフォルトの http_port はコメントアウト済みです。(# http_port 3128 )
今回は、squidとWebサーバは同じサーバ上にあって、
Webサーバ側は localhost:8080 で受信し、httpsは受けません(squidが受けます)。
それらを踏まえて
http_port で 80(http) と 443(https)をそれぞれ設定しています。 cache_peer でWebサーバ 8080 への接続を設定します。
「cach_peer login=~」はWebサーバ側でdigest認証等を行う際に必要になります。*3

あと、「https_port (中略)protocol=http」 と付けていたんですが、
squid4.2にした時に https接続ができなくなってしまったので、消しました。(詳細?は後述)
さらに(どのタイミングか分かりませんが、すくなくともsuqid-4.2では)
証明書の指定方法が上記記入例の cert, key から tls-cert, tls-key に変わっています。


後半はアクセス許可設定です。
事前に、前の行にデフォルトで記載されていた 「deny all」行 はコメントアウト済みです。
(・・・という具合に、アクセス許可設定は記述順も忘れずに)
自分のサーバ(null-i.netと、そのサブドメイン*4)へのアクセスのみを許可して、
それ以外の squid へのアクセスは拒否します。

以上が基本的な設定でしょうか、
で、
ここからは、
私は諸事情で以下のように追加

cache deny all

まだキャッシュサーバとして使う予定がないので無効化しました。
性能面で必要になったら変更しますが、それまで毎回WebサーバへWebコンテンツを取りに行かせます。
もし cgi 等が上手く動かない場合は一旦キャッシュ無効化して実験すると、問題切り分けに役立つかもしれません。

error_directory /usr/local/squid/etc/err_page

squidのエラーメッセージは丁寧&各国対応で素敵ですが、
私的なリバースプロキシには、いささか豪華というか親切すぎます。
(今回は外部からの不正接続っぽい 404 Not Found を止めるのが目的なので、
サーバ名、メールアドレス、squidバージョンなどの情報は応答する必要がありません)
一通り刷新して、error_directory でカスタマイズします。

# cd /usr/local/squid
# mkdir etc/err_page
# for ff in share/errors/en/*; do
    ee=`basename $ff`
    echo "<html><body>Sorry, you can't access. ($ee)</body></html>" > etc/err_page/$ee
  done

・・・エラーメッセージがいささか不親切すぎ?ですが(いやいや、あくまで例ですよ?)
for 文で、既存のerr_page を参考に
用意しなければならないファイル(≒エラーの種類ですね)を既存設定からピックアップしながら、
echo で 仮 HTMLファイルを作成しています。

httpd_suppress_version_string on

エラーメッセージに書き込まれる squid のバージョンを隠します、が、
今回エラーメッセージファイルは刷新しているので、不要かも。

logformat httplog %tl %>a [%>Hs] %Ss %>st %<st [%>rm %>ru ] [%{User-Agent}>h] [%{Referer}>h]
access_log daemon:/usr/local/squid/var/logs/access.log httplog
logfile_rotate 30

ログの書式です。(まだ更新中)
ソースファイルに同梱の etc/squid.conf.documented にも書いてありますが、
デフォルトは logformat squid が指定されているようです。
少なくとも、その日時「%ts.%03tu」(エポックタイム。unixの秒数)を %tl 書式に変えたかったのです。
また、ログのローテーション(後述)も30日を想定して、保持する数を増やしました。

acl bad_url urlpath_regex ^/[a-zA-Z0-9]+$ (login|setup|admin|bbs|waw|block|proxy).(cgi|php|pl|sh) .action$
http_access deny bad_url
deny_info TCP_RESET bad_url

存在しないセットアップファイルを漁るアクセスを切断します。
フィルタの内容は regex・・・つまり正規表現で記載できます。
(正規表現をどうこね回すかについては 「fail2banのフィルタどうしよう」 の件も参照ください)
記述が順不同になってしまいましたが、
これらは前述のルール「acl ~ dstdomain」よりも「前」に書きます。前、です。
acl urlpath_regex で拒否するPATHの正規表現を設定
(実際はもう少し色々と書いています、記述では雰囲気が伝わればうれしいです)、
その次の http_access deny で拒否します。
deny_info は直前の指定ルールについて(この場合 bad_url)拒否メッセージを指定しますが
この場合はTCP接続をRESETする、つまりエラー画面すら返さず切断(TCP RESET)という素敵な動作を指定できます。
(これがやりたくてsquidを導入しました)

そして、おまじない

udp_incoming_address localhost

これを「やらない」場合、私の手元のsquid バージョンでは、
グローバルアドレス向けのUDPポートが2つも開いているじゃありませんか・・・*5

(netstat -na の結果より、身に覚えのない 33841 と 58930 が・・・)
udp        0      0 0.0.0.0:33841           0.0.0.0:*
udp6       0      0 :::58930                :::*
# lsof -i:33841,58930
COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
squid   12663 nobody    6u  IPv6 384082      0t0  UDP *:58930
squid   12663 nobody    7u  IPv4 384083      0t0  UDP *:33841

ポートはランダムに変わるようです、とはいえ、
(うちの場合は外向けのUDP通信なんぞ想定していないので)
勝手に外へのポートを開かれては困ります、ということで先ほどの
udp_incoming_address localhost で内部通信へ切り替えておきます。

(ふたたび netstat -na の結果より、localな通信になっていることを確認)
udp        0      0 127.0.0.1:39731         0.0.0.0:*


更におまじない2、必要に応じて設定します。

udp_outgoing_address DNS問合せ等に使うインタフェースのアドレス

えっと、つまりDNSサーバとか(宛先)ではなく、自分側のIPアドレス(送信元)を指定します。
これは以下のようなエラーが出る場合に設定します(x は適当な数字)

comm_udp_sendto FD x, (family=2) xx.xx.xx.xx:53: (22) Invalid argument
idnsSendQuery FD x: sendto: (22) Invalid argument

xx:xx:xx:xx:53 のところは、例えば /etc/resolv.conf の nameserver が入ったり、
つまりDNS(=53)に失敗?しているというエラーログが出てうっとうしい場合です。
wiki.squid-cache.org でその答えがあるんですが、
とくていの環境下(running Squid in the jail environment *6)の方々は
udp_outgoing_address を指定すれば解決するよ、と。


(以上、ずいぶん色々設定しましたが、
 バージョンが変わったらまた、設定内容も変わると思います)

設定出来たら、squidを再起動します。
私の環境の場合は、Webサーバも同じサーバなのでそちらの設定更新&再起動が先ですが。

Webサーバ側の変更(apache)

前述の通り、同一サーバ上にある apache の設定を変えます。
まず、受信ポート 80 はsquid に譲って、squidから8080で受けるので。

# Listen 80            # こちらは削除
Listen localhost:8080  # こちらへ変更。localhost にすることも忘れずに。

それから今回は https を無効にしました。
私の環境の場合は「httpd-ssl.conf」の読込をコメントアウト。

そして、LogFormat の 「&h」を「%{X-Forwarded-For}i」に変更します。*7
そのまま(%h)では全部 squidサーバのアドレスになってしまうので。

また、
vhostも使っている場合は、httpd-vhosts.conf の変更もお忘れなく。
私の場合は サブドメイン subdomain.null-i.net 用の変更が必要でした。

一通りできたら Webサーバを再開します。
localhostにした場合は外からはアクセスできないので、wget での確認例はこんな感じでしょうか。

# wget localhost:8080      # あるいは localhost:8080/任意のページ.html とか

squid の起動、停止、ログローテーション

とりあえず、起動。

/usr/local/squid/sbin/squid -f /usr/local/squid/etc/squid.conf

WebサーバとSquidの両方を起動して、Webアクセスができることを確認します。
起動&停止オプション(-k shutdownなど)については「squid -h 」で確認できます。

また、「-k rotate」でログをローテーションできます。
これもsquid-cache.org の情報を参考に、cron で回すことにしました。

# crontab -e
(日付変更時にローテーションさせる例)
0 0 * * * /usr/local/squid/sbin/squid -k rotate

あとは、起動スクリプトですね。 「/etc/init.d/squid 」を他の起動スクリプトを参考に、 まずは start、stop だけでも実装すれば良いかと。

(ほんとうに start、stop だけの例)
#!/bin/sh
#chkconfig: 2345 90 40
SQUID_BIN=/usr/local/squid/sbin/squid
SQUID_CONF=/usr/local/squid/etc/squid.conf
case $1 in
start)
   $SQUID_BIN -f $SQUID_CONF
   ;;
stop)
   $SQUID_BIN -k kill
   ;;
esac

まず、「stop」 は shutdown にするのがベターかも。*8
chkconfig は、apache など Webサーバの後に起動(90)、前に停止(40)するように数字を調整します。
(それを踏まえて、もう少し上記をまじめに作ったものを、)
「chkconfig --add squid 」で /etc/rc.d へ登録します。する予定です。するといいな。


これでひとまず動かしてみます。
あとは fail2ban のログ監視も apache とすみ分けるように変える予定です。
前述の deny_info TCP_RESET するログなんかは、百年くらいbanしてやれば良いんじゃないかな。

余談~URLを日本語へ戻す

ログ出力される URLの日本語変換が apache と違うんですね。squidは「%」もエスケープ?なんでだろ?
ブラウザ上でURLへ「index.html?ぬるいねっと」と入力すると、
apacheのログでは「index.html?%E3%81%AC%E3%82%8B~」に、
squidのログでは「index.html?%25E3%2581%25AC%25E3%2582%258B~」に更にエスケープされます。

apacheの %E3%81%AC は「ぬ」に変換できるのですが
(2行とも、「ぬ」へ変換するコマンドの例です)

echo %E3%81%AC | perl -MURI::Escape -pe 'print uri_unescape $_'
echo %E3%81%AC | perl -e 'while(<STDIN>){ s/%([0-9a-fA-F]{2})/pack("H2",$1)/ge; printf("%s\n", $_); }'

squidの %25E3%2581%25AC は「%25」を一旦「%」に戻してからでないと変換できませんね。

echo %25E3%2581%25AC |  perl -MURI::Escape -pe 's/%25/%/g; print uri_unescape $_'
echo %E3%81%AC  |  perl -e 'while(<STDIN>){ s/%25/%/g; s/%([0-9a-fA-F]{2})/pack("H2",$1)/ge; printf("%s\n", $_); }'

wikiなのでURLを日本語に戻さないとさっぱり分からないのですが、
squidのアクセスログが日本語に戻らなかったので少し焦りました。まさか%が変換されるとは・・・

余談〜 SSLで 400 BAD になる件

squidを3.5から4.2に上げたら、Webブラウザからのhttps接続で「ERR_SSL_PROTOCOL_ERROR」になってしまいました。
結論を言えば、squid4.2では、https_port accel で「protocol=HTTP」とかにするとNGっぽい(なるほど、さっぱり分かりません)なのですが、
どちらかというと、その回答に至ったまでをメモとして残しておきます。

とりあえず、Webブラウザではさっぱり分からないので、
クライアントをopenssl にして debugオプションをつけてみます。

$ openssl s_client -connect null-i.net:443 -debug
CONNECTED(00000005)
write to 0xe30ff0 [0xe3f8e0] (314 bytes => 314 (0x13A))
0000 - 16 03 01 01 35 01 00 01-31 03 03 5b a9 70 de 9e   ....5...1..[.p..
(中略)SSL routines:ssl3_get_record:wrong version number:ssl/record/ssl3_record.c:332:

みたいになります。
でも、このメッセージだけではほぼノーヒントみたいなもので
SSLのバージョンとかの問題かな?と最初は思いました。
Web検索してヒットする話題もそれが多かったので、その線で色々試しましたが、まだ原因は分かりません。

それで、上では省略していますが、
このあとに続くメッセージが「HTTP/1.1 400 Bad Request」になっていて、
HTTPの400番代のエラーはクライアントエラーなので(500番代がサーバ側)
少なくともsquidは、悪いのはクライアント側だと言っています。

これ、そもそもSSL接続出来ているのか?と思って試しに

$ openssl s_client -connect null-i.net:80 -debug
(ポート80なので、当然 https接続はできない)

とやってみると、ほぼ同じエラーになります。
つまり、https(443)に接続しているのに http(80)接続扱いされているらしい。
それで、openssl は wrong version(そもそもSSLのバージョンが不明というか、SSLではない)と言っているし、
squid は 400 Bad Request(そもそもHTTPではない)と言っていたようです。

そこで、サンプルを見ながら、https_port 行の順番を変えてみたり
wiki.squid-cache.org で、絶対に順番は前だぞ、と念を押されています)
OpenSSLのバージョン変えてmakeしなおしたりしましたが、解決せずに、
なんとなく、
ノリで「https_port(中略)protocol=http」が(httpって言ってるから)消してみたら
ちゃんと接続できるようになりました*9

別に「protocol=http」でも今までは上手く行ってたし、
そもそもこのオプションってsquidが拾ったリクエストをどのprotocolでreconstruct(さいこうちく)するかって話ではないの? 400 Bad?
...なるほど、さっぱり分かりません、という結論に至りました。

ただ、強いて言えば、
squidは wiki.squid-cache.org で設定例が多く紹介されているので(英語ですが)、
とりあえずそれの真似をしてみると上手く行く可能性、は、あります。

tls-certも こつぜんと名前変わっちゃった印象があるし、パラメータ変更は結構多い気がするので、
squid-cache.org から都度つど設定例を追っていった方が良さそうだ、とあらためて思い知りました。


*1 後述していますが、今回は「deny_info TCP_RESET」を利用して、念願の「即切断」を実現できました。
*2 そして2018/09現在では4.2版へメジャーバージョンも上がって、パラメータもいくつか変わっています。早っ。ちなみに私は新旧のetc/squid.conf.documentedを diff -y で比較してパラメータの増減を確認しました。
*3 他の注意点についても squid-cache.org の Reverse Proxy 「Common Problems」 の項目を見た方が良さそうです。cgiの場合の注意とか。
*4 www. がWebブラウザで自動で付与される場合も考慮しなければならないのが盲点でした・・・
*5 あとで気づきましたが、これは ICPや DNS等に利用するポートで、デフォルト値は all interface だと squid.conf.documented に書いてありました。幸いうちの firewall は外との通信を許可していませんでしたが。
*6 jail? つまりWeb接続を制限しているような閉じた環境でsquidを使っているか、牢獄のような日々を送っている人か、私の場合は後...前者というかFireWallで色々切っているのが影響したのかも?
*7 X-Forwarded-Forはプロキシなどで転送した時に転送元を記録するHTTPヘッダ。今回は(Squid 3.5 で)自動で付けてもらえます。
*8 shutdown を使った init.d の実装例は squid-cache.org を参照、停止完了まで while ループさせています。あとは 引数チェックやら コマンドの戻り値($?)の確認やらがあれば良いかも?
*9 「protocol= parameter」の扱いが3.5.01で変わったって言っているので、3.5.24はちゃんと動くから関係無いよな?と思いつつ、消してみたら...

  最終更新のRSS