認証ありプロキシを認証なしにするDockerコンテナ

# はじめに

皆さんの会社では、社内プロキシを利用していますか？
私のところでは認証ありの社内プロキシ利用が必須となっており、設定しなければ外に出て行くことができません。

セキュリティやキャッシュ等、プロキシには良い面が多いですが、様々なツールを使い始めようとする際に障壁となることも多いです。
私が配属先で最初に教わったのは、これから幾度も立ちはだかるであろうプロキシの壁をいかに突破するかです:innocent:

社内で、なんか外に繋がらないなーと思ったら、ほぼ間違いなくプロキシを設定し忘れたかミスっているかのどちらかです。
厄介なことに、ツールによってプロキシの認証設定の方法が異なっていたり、また、モノによっては認証IDに含まれている“@”を“%40”に変えないといけないというトラップがあったりもします。
（私のところでは認証IDがメールアドレスなので、見事にハマりました:disappointed_relieved:）

それと、アプリケーションの中には認証なしのプロキシしか対応していないモノも存在します。
例えばandroidコマンドは、ProxyサーバのIP&Portを設定するオプションはあるけど、ID&PWを指定するオプションはないので、SDKを自動インストールすることができませんでした。（実際は、対話形式でID&PWの入力できるので、認証なししか対応していないというのは正しい表現ではありませんが、認証を自動でおこなってくれないということで例に挙げました。）
また、Androidアプリの中には、プロキシには対応しているけれども認証処理に対応していないモノもあります。

そこで、認証ありプロキシを認証なしにするDockerコンテナを使うことで、煩わしいプロキシ設定を単純化したり、認証に対応していないアプリケーションを利用できるようにしたりします。

# やりかた

簡単に言うと、squidによる多段プロキシです。

## 構成
『認証ありプロキシ』への認証を代理でおこなってくれる、『認証なしプロキシ』Dockerコンテナを立ち上げます。
そして、クライアントで『認証なしプロキシ』をプロキシサーバとして設定することで、クライアント→『認証なしプロキシ』→『認証ありプロキシ』→インターネット、といったように外に出て行きます。

![構成図.png](https://qiita-image-store.s3.amazonaws.com/0/125684/695e295d-42c2-6683-f63d-edf5d6dcdac8.png)

## Dockerfile

```dockerfile:Dockerfile
FROM alpine:3.9

# Default vars
ENV PARENT_PROXY_HOST=proxy.example.com \
    PARENT_PROXY_PORT=8080 \
    PARENT_PROXY_ID=hoge@example.com \
    PARENT_PROXY_PW=hogepasswd

# Install package
RUN apk --no-cache add squid gettext

EXPOSE 3128

# Create template conf
RUN { \
      echo 'http_port 3128'; \
      echo 'acl all_ports port 0-65535'; \
      echo 'acl CONNECT method CONNECT'; \
      echo 'http_access allow all_ports'; \
      echo 'http_access allow CONNECT all_ports'; \
      echo 'http_access allow all'; \
      echo 'never_direct allow CONNECT'; \
      echo 'cache_peer ${PARENT_PROXY_HOST} parent ${PARENT_PROXY_PORT} 0 no-query no-netdb-exchange login=${PARENT_PROXY_ID}:${PARENT_PROXY_PW}'; \
      echo 'shutdown_lifetime 5 seconds'; \
      echo 'logfile_rotate 0'; \
      echo 'access_log stdio:/dev/stdout squid'; \
      echo 'pid_filename /var/run/squid/squid.pid'; \
    } > /etc/squid/squid.conf.template \
    && chown -R squid:squid /etc/squid

# Create endpoint script
RUN { \
      echo '#!/bin/sh -e'; \
      echo 'if [ $1 = "squid" ]; then'; \
      echo '  envsubst < /etc/squid/squid.conf.template > /etc/squid/squid.conf'; \
      echo '  set -- /usr/sbin/squid -f /etc/squid/squid.conf -N -d 1'; \
      echo 'fi'; \
      echo 'exec "$@"'; \
    } > /usr/local/bin/docker-entrypoint.sh \
    && chmod +x /usr/local/bin/docker-entrypoint.sh

ENTRYPOINT ["docker-entrypoint.sh"]

USER squid

CMD ["squid"]
```
“Default vars”のところで指定している認証ありプロキシサーバ(PARENT_PROXY)のhost, port, ID, PWは適切に書き換えてください。
あるいは、Run時に`-e`で指定できるので取り除いてしまっても構いません。

squidの設定ファイルやendpointのスクリプトはDockerfileとは別に用意した方がお行儀がいいのかもしれませんが、~~めんどくさいし~~内容が短いので、Dockerfile内で生成するという方法をとっています。
（最近知りましたが、OpenJDKのOfficialリポジトリの[Dockerfile](https://github.com/docker-library/openjdk/blob/2f9d8d32f19425da337cf5793bdffda6909f5fec/9-jre/Dockerfile#L30)内でも同じ書き方しているので、悪い書き方ではないのかもしれませんね）
それと、ベースをalpine:3.9に固定していますが、別にlatestで構わないですし、alpineじゃなくてもいいです。

説明も何もいらないと思いますが、Dockerfile内では主に以下のことをおこなっています。
1. squidをインストール
2. squidの設定ファイルのテンプレートを書き出す
3. 実行スクリプトを書き出す

squidの設定は、全ポートへのアクセスが可能、かつ全ポートのCONNECT要求も受け入れるようにしています。
なぜこうしたかというと、

> 全ポートへのアクセスが可能、

http\://example.com:8080 といったように80ポート以外にもアクセスしたい場合があるから。

> かつ全ポートのCONNECT要求も受け入れる

443ポート以外でも、SSLやWebSocketを使用したい場合があるからです。

簡単に言うと、上位プロキシである『認証ありプロキシ』が許す限り、インターネット上のサイトのどんなポートでもアクセスできるし、httpsでもOKだし、WebSocketも使えるようにしています。

最終的に上位Proxyが面倒を見てくれるので、全ポート通しても構わないと思いますが、もし、80/443ポートのhttp/httpsのみに制限したい場合は、15行目あたりを以下のように書き換えてください。

```dockerfile
# Create template conf
RUN { \
      echo 'http_port 3128'; \
      echo 'acl SSL_ports port 443'; \
      echo 'acl Safe_ports port 80'; \
      echo 'acl CONNECT method CONNECT'; \
      echo 'http_access deny !Safe_ports'; \
      echo 'http_access deny CONNECT !SSL_ports'; \
      echo 'http_access allow all'; \
      echo 'never_direct allow CONNECT'; \
      echo 'cache_peer ${PARENT_PROXY_HOST} parent ${PARENT_PROXY_PORT} 0 no-query no-netdb-exchange login=${PARENT_PROXY_ID}:${PARENT_PROXY_PW}'; \
      ...省略...
```

(2019/04/23 修正)
`docker stop`した際に、Exit Codeが0ではなく137(= 128 + 9(SIGKILL))になってしまう問題があったので、設定ファイルとendpointスクリプトの内容を修正しました。

## docker build

```bash
$ docker build \
    -t yama07/proxy-proxy \
    --build-arg http_proxy=$http_proxy \
    --build-arg https_proxy=$https_proxy \
    .
```

Dockerイメージ名等はご自由に変更してください。

## docker run

```bash:実行例
$ docker run \
    -d \
    -p 8080:3128 \
    -e PARENT_PROXY_ID=yama@example.com \
    -e PARENT_PROXY_PW=yamapasswd \
    --name yamaproxy \
    yama07/proxy-proxy
```

オプションは以下の通りです。

- ` -p my_listen_port:3128`：　認証なしプロキシ(Dockerコンテナ)の待ち受けポート
- `-e PARENT_PROXY_HOST`：　認証ありプロキシのIPアドレス
- `-e PARENT_PROXY_PORT`：　認証ありプロキシのポート
- `-e PARENT_PROXY_ID`：　認証ありプロキシのID
- `-e PARENT_PROXY_PW`：　認証ありプロキシのパスワード

# 使い方
Dockerコンテナ起動後、各ツールに応じたProxy設定をしてください。
このとき、当然ですが、Dockerコンテナで動かしている認証なしのプロキシの方をプロキシサーバとして設定してください。
あとは、認証設定なしで外にアクセスできるようになっています。

```bash:使用例
$ export http_proxy=http://DockerホストのIP:my_listen_port
$ wget http://www.google.co.jp/

$ export https_proxy=http://DockerホストのIP:my_listen_port
$ wget https://www.google.co.jp/
```