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

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


補足:
今はメモにそってソースからビルドしなくても、パッケージからインストールでも良いと思います。
CentOS Stream8 での手順はこちら


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

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

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

のように、通信の間でSquidが転送 or 切断してあげるイメージです。
Webブラウザからのリクエストを転送、ではなく、
Webサーバからのレスポンスを転送というのが「リバース」なプロキシです。

リバースプロキシの印象って、なんか、こう、
数万いるユーザからのアクセスを複数のWebサーバでさばくために運用したりするイメージがあるのですが、
今回の導入目的は、
Squidで不正アクセスに 404 応答あるいは即切断(TCP_RESET)を実行することにあります。

ソースの取得とビルド

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版ですが、*1
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認証等を行う際に必要になります。*2

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


後半はアクセス許可設定です。
事前に、前の行にデフォルトで記載されていた 「deny all」行 はコメントアウト済みです。
(・・・という具合に、アクセス許可設定は記述順も忘れずに)
自分のサーバ(null-i.netと、そのサブドメイン*3)へのアクセスのみを許可して、
それ以外の 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つも開いているじゃありませんか・・・*4

(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 *5)の方々は
udp_outgoing_address を指定すれば解決するよ、と。


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

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

Webサーバ側の変更(apache-httpd)

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

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

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

そして、LogFormat の 「&h」を「%{X-Forwarded-For}i」に変更します。*6
そのまま(%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 にするのがベターかも。*7
chkconfig は、apache など Webサーバの後に起動(90)、前に停止(40)するように数字を調整します。
(それを踏まえて、もう少し上記をまじめに作ったものを、)
「chkconfig --add squid 」で /etc/rc.d へ登録します。する予定です。するといいな。


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

送信元アドレスによるWebサーバの振り分け

前述のような設定も終わり、しばらく後に追加した設定です。

もう一台、Webサーバを「localhost:8081」として追加してから、
特別なお客様方(vip_guests)はそちらのWebサーバへご案内しよう、という遊びおもてなしです。

あらかじめ「etc/blacklist.txt」のような
IPアドレスorサブネット一覧を書いておいたブラックリストお客様名簿を作っておいてください。
(ぶっちゃけ、明らかにアウトなアクセスが連続するなら、
 いっそ別のWebサーバに振っちゃえ、という例です)*8

前に書いた「cache_peer」行を一旦全て削除したうえで、
aclルール行の最後を、以下の形に変更しました。

# acl web_client dstdomain あなたのドメイン
# http_access allow web_client
# ...という感じの行が、事前に書いてある状態から

# VIPは 8081へご案内します
cache_peer localhost  parent 8081 0 no-query originserver login=PASS name=my_vip
acl vip_guests src "/usr/local/squid/var/etc/blacklist.txt"
cache_peer_access my_vip allow vip_guests
cache_peer_access my_vip deny all

# それ以外は 8080へご案内します
cache_peer localhost  parent 8080 0 no-query originserver login=PASS name=my_accel
cache_peer_access my_accel allow web_client
http_access deny all

私がつまずいたのは2点、
まず「acl  ACL名   src  "ファイル名"」のダブルクォーテーション(")を忘れず。
囲まなくても(その場合はファイルではなく、パラメータ扱いで)起動できちゃうので、見落としに気が付かなかったです
それと「cache_peer_access ~ allow ~」だけではなく「cache_peer_access ~ deny ~」も指示しないと、
すべてのアクセスを先に書いたcache_peer_access(上の例だと vip_guests)だけで拾ってしまうかもしれません。
この辺はACLの書き方、順番によって変わるかもしれませんが、
なんどやっても振り分けできない!と結構苦戦してしまいました...

acl ~ srcで指定したファイルを更新した場合は、
squid -k reconfigure 等で反映しましょう。


補足として、こうやって振り分けることの効能ですが、
面白いからやってますリファラースパム(アクセス元に自分のいかがわしいサイトをつけて宣伝してくるアレ)が大幅に減りました。
彼らからうちのサイトを見たときに、毎回雑な偽トップページが表示されるので、
うちのサイトが更新されていないor閉鎖されたと判断するのだと思います。
すると彼らは早々に他の効率の良いスパムの投げ先を探すのでしょう。そう、時間もコストも有限です、がんばれ。
あと、こちらも正規のアクセスログが減るので、ログを見る気が起きるようになります。
ちゃんと遊び以外の効果もあったのです!

余談~URLを日本語へ戻す

ログ出力される URLの日本語変換が apache-httpd と違うんですね。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 から都度つど設定例を追っていった方が良さそうだ、とあらためて思い知りました。

余談〜 transaction-end-before-headersな話

アクセスログによく出て来る、これ。

error:transaction-end-before-headers

これ自体は通信の中断なので、
途中でWebブラウザを閉じたとか、スマホの電波が途中で届かなくなったとか、原因は色々あると思うのです。
別ログで似たような通信中断ログは、まぁ、色々あると思います。
ただ...


話が逸れますが、
ssh接続するポートをデフォルト(22)以外に変える事で、不正アクセスを大幅に減らすことができました。
lastbのログイン失敗履歴表示が格段に減って見やすくなったのは、地味に大きいメリットです。
でも、...減ったのですが、無くなったりはしません。
あんまりにもしつこい攻撃について、そのアドレス帯で

例えば192.168.xx.yyなら、最後のyyを除いた 192.168.xxや、192.168で探します。
しつこいやつはレンタルサーバ等で、複数のサーバから攻撃してくるのがパターンの1つなので。

試しにsquidのログをgrep検索してみたら、残念、いっぱい居ました。
そのIPアドレスからのアクセスが、
「error:transaction-end-before-header」やら「error:invalid-request」やらで。
つまり、ポートスキャンやら脆弱性探しやらのアクセスもend-before-headerに含まれるようです。


話を戻して、
やはり end-before-headerなんかも、頻発するなら攻撃の可能性・兆候ととらえた方が無難ですね。
仮にそれがbotではなく、ふつうにWebブラウザからのアクセスだったとしても、頻発させるなら攻撃しているのと一緒な訳ですし。


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