mac上にHTTPプロキシを立てることで認証プロキシの認証をスキップする

社内から社外にhttp(s)で出る場合に、何かある度に認証プロキシに対して認証情報を入れる必要があって邪魔だったため、mac内に立てたプロキシに認証を代替してもらうようにしてみました。

ついでに、プロキシの設定をいろんな箇所に設定するのも面倒だったため、透過プロキシの設定もしてみました。ただし、最終的にはHTTPSの場合には独自の証明書を設定する必要があったため、HTTPのみ透過プロキシを介するようにしています。

# 環境

* プロキシ認証が必要な環境
* OS X El Capitan
* Homebrew 0.9.9

# 手順

## squidユーザーの追加

プロキシとして動作するsquidプロセスをsquidユーザーで動作させたいため、squidユーザーを「システム環境設定」の「ユーザとグループ」から追加します。

## squidのインストール

Homebrewでインストールします。

```bash
$ brew install squid
==> Downloading https://homebrew.bintray.com/bottles/squid-3.5.15.el_capitan.bottle.1.tar.gz
Already downloaded: /Library/Caches/Homebrew/squid-3.5.15.el_capitan.bottle.1.tar.gz
==> Pouring squid-3.5.15.el_capitan.bottle.1.tar.gz
==> Caveats
To have launchd start squid at login:
  mkdir -p ~/Library/LaunchAgents
  ln -sfv /usr/local/opt/squid/*.plist ~/Library/LaunchAgents
Then to load squid now:
  launchctl load ~/Library/LaunchAgents/homebrew.mxcl.squid.plist
==> Summary
🍺  /usr/local/Cellar/squid/3.5.15: 2,061 files, 9.5M
```

## squid.confの編集

デフォルトの設定から以下を変更しています。

```bash
$ diff $(brew --prefix)/etc/squid.conf.default $(brew --prefix)/etc/squid.conf
4a5,6
> cache_effective_user squid
> 
14a17
> acl SSL_ports port 2376
59c62,63
< http_port 3128
---
> http_port 8080
> http_port 8081 transparent
62a67,69
> no_cache deny all
> cache_dir null /dev/null
> cache_store_log none
73a81,96
> 
> visible_hostname <ホスト名>
> hosts_file /etc/hosts
> 
> acl localdomain dstdomain .example.co.jp
> acl localdomain dstdomain .example.com
> acl localdomain dstdomain .example.local
> acl localseg dst 10.0.0.0/8
> acl localseg dst 172.16.0.0/12
> acl localseg dst 192.168.0.0/16
> always_direct allow localdomain localseg
> 
> cache_peer <上位のプロキシサーバ> parent 8080 0 no-query login=<プロキシユーザー>:<プロキシパスワード>
> cache_peer_access <上位のプロキシサーバ> deny localdomain
> cache_peer_access <上位のプロキシサーバ> deny localseg
> never_direct allow !localdomain !localseg
```

主な設定項目の説明です。

* `cache_effective_user squid`:<br>squidプロセスをsquidユーザーで動作させるための設定です。後述する透過プロキシのためのポートフォワード設定で、ユーザーに応じて転送先を変更するために追加しています。
* `acl SSL_ports port 2376`:<br>デフォルトでは、ポート443以外のHTTPS通信を許さない設定となっていたのですが、ポート2376で動作するプログラムがいたため、追加しています。
* `http_port 8080`:<br>通常のプロキシをポート8080で動作させています。
* `http_port 8081 transparent`:<br>透過プロキシをポート8081で動作させています。
* `always_direct allow localdomain localseg`:<br>ローカルのドメイン(この例では`.example.(co.jp|com|local)`)とローカルのセグメント(10.0.0.0/8, 172.16.0.0.12, 192.168.0.0/16)については、上位のプロキシを介さずに直接通信することを許可します。
* `cache_peer <上位のプロキシサーバ> 8080 0 no-query login=<プロキシユーザー>:<プロキシパスワード>`:<br>上位のプロキシの転送設定です。`login=`にてプロキシ認証用のユーザーとパスワードを指定しています。
* `cache_peer_access <上位のプロキシサーバ> deny localdomain|localseg`: ローカルドメインとローカルセグメントについては、上位のプロキシサーバへの転送を拒否します (4/14追記)
* `never_direct allow !localdomain !localseg`: ローカルドメインとローカルセグメント以外は、直接接続を拒否します (4/14追記)

4/14追記:
`allow_direct`を指定しておけば直接接続されるかと思っていましたが、実際には`cache_peer_access`で拒否しない限りは上位のプロキシに接続しに行ってしまっていたため、修正しました。squidの設定、難しい。。。

## squidの起動設定

インストール時のメッセージにはユーザーのエージェントとして起動するように記載されていましたが、`squid`ユーザーで動作させるためにデーモンとして設定しています。

起動のための設定ファイルをシステムにコピーします。

```bash
$ sudo cp $(brew --prefix squid)/homebrew.mxcl.squid.plist /Library/LaunchDaemons
```
LaunchDaemonにsquidをデーモンとして登録します。

```bash
$ sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.squid.plist
```

## システムのプロキシ設定

システム環境設定のネットワークのHTTP(S)プロキシの設定に`localhost:8080`を設定すれば、認証情報を入力することなくプロキシを利用できるようになります。

これで当初の目的は達したのですが、VMやらコンテナやらを作成するたびに`http_proxy`環境変数を設定するというのも面倒なので、以降では透過プロキシを設定します。

## ポートフォワードの設定

80ポートに対して外部に出て行く通信を、squidに転送するように設定します。これを実現するために、pf (Packet Filter)を利用します。

### pf.confの編集

`/etc/pf.conf`から転送用の設定を読み込むようにします。
(5/13修正) 以下の行を`/etc/pf.conf`に追加しています。`rdr`ルールの後ろに各種フィルタールール(`anchor`で読み込まれる`pass`等のルール。)を記載する必要があるため、もし他のルールが存在している場合、`anchor`等の前に`rdr-anchor`を記載する必要があります。

```:/etc/pf.conf
# ...
rdr-anchor "proxy"

# ...
anchor "proxy"
load anchor "proxy" from "/etc/pf.anchors/proxy"
```

`/etc/pf.anchors/proxy`を以下のように編集します。

```:/etc/pf.anchors/proxy
ext_if = "en0"
table <rfc1918> const { 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 }

rdr on lo0 inet proto tcp from any to any port 80 -> lo0 port 8081
pass out quick on $ext_if inet proto tcp from any to <rfc1918>
pass out on $ext_if route-to lo0 inet proto tcp from any to any port 80 user != squid
```

この設定により、以下のような動作をします。

1. NIC `en0`上のポート80に対して外部に出て行く通信を、ループバックインターフェース`lo0`に転送します。
2. ループバックインターフェース`lo0`上のポート80に対する通信を`squid`に転送します。

単純に外部に出て行く通信を`squid`に転送するだけだと、`squid`から出て行く通信も再度転送されてしまうため、`user != squid`の条件により`squid`ユーザーで動作するプロセスからの通信は転送しないように設定しています。

(4/15追記) また、pfからsquidに転送後、上位プロキシに転送せずに直接接続した場合にうまく接続できていなかったようなので、社内 (192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8) への通信はsquidに転送しないような設定も追加しています。この追加により、squidプロセスをsquidユーザーで動作させる必要がなくなっている気もしますが。

### pfの起動設定

pfをデーモンとして起動させるには、自分で設定を記述する必要があります。これは参考に記載したブログの設定をほぼそのまま利用させていただきました。

```xml:/Library/LaunchDameons/pf.plist
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE plist PUBLIC "-//Apple Computer/DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>pf.plist</string>
    <key>Program</key>
    <string>/sbin/pfctl</string>
    <key>ProgramArguments</key>
    <array>
      <string>/sbin/pfctl</string>
      <string>-e</string>
      <string>-f</string>
      <string>/etc/pf.anchors/proxy</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>ServiceDescription</key>
    <string>FreeBSD Packet Filter (pf) daemon</string>
    <key>StandardErrorPath</key>
    <string>/var/log/pf.log</string>
    <key>StandardOutPath</key>
    <string>/var/log/pf.log</string>
  </dict>
</plist>
```
LaunchDaemonにpfをデーモンとして登録します。

```bash
$ sudo launchctl load /Library/LaunchDaemons/pf.plist
```

## ファイアウォールの設定

本環境にはセキュリティ対策ソフトとしてMcAfee Endpoint Protection for Macが入っているのですが、どこかの通信をブロックしてしまっているようです。とりあえずはファイアウォールを無効化することで回避しましたが、うまくルールを設定できたら、情報を追記したいと思います。

## DNSの設定

弊社だけかもしれませんが、社内用のDNSサーバに問い合わせても社外のドメイン名を解決してくれません。普通にプロキシを設定している場合には特にこれで困ることはないのですが、透過プロキシの場合には、社外の名前も解決できないとうまく動作しませんでした。

そこで、dnsmasqを使って社内と社外の両方の名前を解決してくれるdnsサーバを立てました。なお、以下ではうまくいきませんでした。

* 単純にネットワーク設定に複数のDNSサーバを設定する<br>
  これは、単純に優先DNSサーバが応答しない時に代替DNSサーバが利用されるというだけなので、この目的では使えません。
* `/etc/resolver`配下に社内用ドメインに対するDNSサーバの設定を記載する<br>
  `/etc/resolver`配下に、`example.com`(社内ドメイン)という名前のファイルに社内用のDNSサーバを設定しておき、全体の設定には社外用のDNSサーバを設定してみました。これで問題ないように見えるのですが、なぜかうまく動作しませんでした。

### dnsmasqのインストール

Homebrewでインストールします。

```bash
$ brew install dnsmasq
==> Downloading https://homebrew.bintray.com/bottles/dnsmasq-2.75.el_capitan.bottle.tar.gz
Already downloaded: /Library/Caches/Homebrew/dnsmasq-2.75.el_capitan.bottle.tar.gz
==> Pouring dnsmasq-2.75.el_capitan.bottle.tar.gz
==> Caveats
To configure dnsmasq, copy the example configuration to /usr/local/etc/dnsmasq.conf
and edit to taste.

  cp /usr/local/opt/dnsmasq/dnsmasq.conf.example /usr/local/etc/dnsmasq.conf

To have launchd start dnsmasq at startup:
  sudo cp -fv /usr/local/opt/dnsmasq/*.plist /Library/LaunchDaemons
  sudo chown root /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
Then to load dnsmasq now:
  sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
==> Summary
🍺  /usr/local/Cellar/dnsmasq/2.75: 7 files, 492.3K
```

### dnsmasq.confの編集

サンプルをベースに設定を変更します。

```bash
$ cp $(brew --prefix dnsmasq)/dnsmasq.conf.example $(brew --prefix)/etc/dnsmasq.conf
```

以下のような設定をしました。

```:/usr/local/etc/dnsmasq.conf
# Never forward plain names (without a dot or domain part)
domain-needed
# Never forward addresses in the non-routed address spaces.
bogus-priv
# Change this line if you want dns to get its upstream servers from
# somewhere other that /etc/resolv.conf
resolv-file=/etc/dnsmasq.resolv.conf
# Add other name servers here, with domain specs if they are for
# non-public domains.
#server=/localnet/192.168.0.1
server=/<社内ドメイン>/<社内用DNSサーバIP>
```

### dnsmasqの起動設定

起動のための設定ファイルをシステムにコピーします。

```bash
$ sudo cp $(brew --prefix dnsmasq)/homebrew.mxcl.dnsmasq.plist /Library/LaunchDaemons/
```

LaunchDaemonにdnsmasqをデーモンとして登録します。

```bash
$ sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist 
```

### システムのDNS設定

システム環境設定のネットワークのDNSの設定に`localhost`を指定すれば、dnsmasqを使って社内外両方の名前を解決できるようになります。

## システムのプロキシ設定

以上の設定により、システム環境設定のネットワークのHTTPプロキシの設定に何も設定しなくても、認証情報を入力することなくプロキシを利用できるようになります。

なお、HTTPS通信についても、証明書を適切に設定すれば透過プロキシを利用できるようなのですが、現状は以下のような理由により設定していません。

* クライアント側に証明書を設定するのが手間である
* Mac上に立てたVMやコンテナから社外に対してHTTPS通信をしなければならないケースはあまりない気がする

4/14追記:
上記のように思ったのですが、実際使っていると結構HTTPS通信が必要なので、いまいち透過プロキシを立てた意味がないです。また余裕があるときにHTTPS通信を透過型にして嬉しいかどうかを実験してみたいと思います。

# 参考

* [NIC1枚で透過型プロキシ](http://blog.nadenadekanade.com/archives/2009/03/nic1.html)
* [Using pf on OS X Mountain Lion](http://blog.scottlowe.org/2013/05/15/using-pf-on-os-x-mountain-lion/)