ども、シリーズ化しても一回か二回で終わってしまう cloudpackかっぱ (@inokara) です。

はじめに

HAProxy は最強の L4 / L7 のオープンソースロードバランサだと思います。特に L7 ロードバランサに関しては個人的には出来ないことは無いのではと思っています。

ということで、今回は HTTP リクエストヘッダから接続元 IP をオリジナルの HTTP ヘッダに挿入してバックエンドのアプリケーションでも接続元 IP を利用した制御が出来るような環境を作ってみたいと思います。

memo

環境と要件

以下のような環境と要件で構築と動作確認を行います。今回は HAProxy に直接するパターンと ELB を経由させた多段 Proxy の環境で試してみたいと思います。
HAProxy で HTTP ヘッダを操作するメモ(1)〜多段 Proxy 環境の場合〜:  構成図

接続元の IP とオリジナルヘッダの名前は以下の通りです。

内容
接続元 xxx.xxx.xxx.xxx
オリジナルヘッダ Original-test-header
アプリケーションサーバーポート番号 8081

HAProxy の設定

今回は HAProxy は以下のように設定しました。

frontend balancer-test
  monitor-uri /alive
  mode http
  option forwardfor
  bind 0.0.0.0:81
  default_backend test_backend

backend test_backend
  http-request set-header Client-Source-IP %[req.hdr_ip(X-Forwarded-For,1)]
  mode http
  option forwardfor
  option httpchk GET /index.php
  server web01 127.0.0.1:8001 inter 3000 check
  server bak01 127.0.0.1:80 check

ポイントは…

  • http-request add-header 又は http-request set-header を利用
  • 接続元の IP は req.hdr_ip オプションでパラメータとして X-Forwarded-For を設定
  • X-Forwarded-For に含まれている IP アドレスから最初の IP を取得する為にサブパラメータとして 1 を指定

req.hdr_ip について

ドキュメント には以下の通り書かれています。

This extracts the last occurrence of header in an HTTP request,
converts it to an IPv4 or IPv6 address and returns this address. When used
with ACLs, all occurrences are checked, and if is omitted, every value
of every header is checked. Optionally, a specific occurrence might be
specified as a position number. Positive values indicate a position from the
first occurrence, with 1 being the first one. Negative values indicate
positions relative to the last one, with -1 being the last one. A typical use
is with the X-Forwarded-For and X-Client-IP headers.

ざっくり意訳すると…

  • HTTP リクエストヘッダから X-Forwarded-For 又は X-Client-IP をチェックして IPv4 又は IPv6 アドレスに変換して値を返す
  • ポジション番号を指定することで複数の値から任意の値を取得することが出来る
  • 1 を指定すると最初の値を返す
  • -1 を指定すると末尾から相対的な値を返す

とのことですので、ちょっとだけ試してみたいと思います。

アプリケーションから見て X-Forwarded-for には以下のような値が入ってきてる場合を想定して確認。

X-Forwarded-For: xxx.xxx.xxx.xxx, zzz.zzz.zzz.zzz, 127.0.0.1

以下のように set-header を設定。

backend test_bk
  http-request set-header Original-test-header %[req.hdr_ip(X-Forwarded-For,-1)]

Original-test-header は以下のような結果となります。

Original-test-header: zzz.zzz.zzz.zzz

後ほど、もう少し詳しく試していきたいと思います。

バックエンドのアプリケーション

バックエンドのアプリケーションは以下のような簡単な PHP プログラムを用意。

 $value) {
    echo "$name: $valuen";
}

アプリケーションと呼ぶのが憚れますが、一応、アプリケーション。リクエストヘッダを画面上、コンソール上に出力します。

パターン 1 HAProxy へ直接アクセス

上記の設定が行われた状態の HAProxy に直接にアクセスしてみると以下のようなレスポンスが得られます。

% http http://${HAProxy}:81/
HTTP/1.1 200 OK
Content-Length: 192
Content-Type: text/html; charset=UTF-8
Date: Sun, 01 Feb 2015 13:26:36 GMT
Server: Apache/2.x.xx
X-Powered-By: PHP/5.x.xx

Host: ${HAProxy}:81
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: HTTPie/0.8.0
X-Forwarded-For: xxx.xxx.xxx.xxx, 127.0.0.1
X-Client: 127.0.0.1
Original-test-header: xxx.xxx.xxx.xxx

Original-test-header には X-Forwarded-For に記録されている IP の中から xxx.xxx.xxx.xxx がセットされています。

パターン 2 ELB を経由して HAProxy にアクセス

上記の設定が行われた状態の HAProxy に ELB を経由してアクセスすると以下のようなレスポンスが得られます。

% http http://xxxxxxxxxxxxxxxxx.ap-northeast-1.elb.amazonaws.com:8081/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 292
Content-Type: text/html; charset=UTF-8
Date: Sun, 01 Feb 2015 13:31:54 GMT
Server: Apache/2.x.xx
X-Powered-By: PHP/5.x.xx

host: xxxxxxxxxxxxxxxxx.ap-northeast-1.elb.amazonaws.com:8081
Accept: */*
Accept-Encoding: gzip, deflate
User-Agent: HTTPie/0.8.0
X-Forwarded-For: xxx.xxx.xxx.xxx, zzz.zzz.zzz.zzz, 127.0.0.1
X-Forwarded-Port: 8081
X-Forwarded-Proto: http
X-Client: 127.0.0.1
Original-test-header: xxx.xxx.xxx.xxx

Original-test-header には X-Forwarded-For に記録されている IP の中から xxx.xxx.xxx.xxx がセットされています。さらに、既存のヘッダを上書き出来るか試してみたいので以下のように HAProxy の設定を行います。

frontend balancer-test
  monitor-uri /alive
  mode http
  option forwardfor
  bind 0.0.0.0:81
  default_backend test_backend

backend test_backend
  http-request set-header X-Client %[req.hdr_ip(X-Forwarded-For,1)]
  mode http
  option forwardfor
  option httpchk GET /index.php
  server web01 127.0.0.1:8001 inter 3000 check
  server bak01 127.0.0.1:80 check

X-Client を接続元の IP を設定してみたいと思います。

% http http://xxxxxxxxxxxxxxxxx.ap-northeast-1.elb.amazonaws.com:8081/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 326
Content-Type: text/html; charset=UTF-8
Date: Sun, 01 Feb 2015 13:35:59 GMT
Server: Apache/2.x.xx
X-Powered-By: PHP/5.x.xx

host: xxxxxxxxxxxxxxxxx.ap-northeast-1.elb.amazonaws.com:8081
Accept: */*
Accept-Encoding: gzip, deflate
User-Agent: HTTPie/0.8.0
X-Forwarded-For: xxx.xxx.xxx.xxx, zzz.zzz.zzz.zzz, 127.0.0.1
X-Forwarded-Port: 8081
X-Forwarded-Proto: http
X-Client: xxx.xxx.xxx.xxx

上記のように X-Client には接続元の IP が記録されていることが解ります。

さいごに

今回得た知見は以下の通りです。

  • HTTP ヘッダの操作は http-request add-header 又は http-request set-header を利用する
  • HAProxy 内で変数っぽい設定は %[req.hdr_ip(X-Forwarded-For,1)] のように %[ ] を利用する
  • 接続元の IP は req.hdr_ip オプションでパラメータとして X-Forwarded-For を設定することで取得出来る
  • X-Forwarded-For に含まれている IP アドレスから最初の IP を取得する為にサブパラメータとして数値を指定することで任意の IP アドレスを取得することが出来る

ということで…またまた HAProxy の奥深さを垣間見ることが出来ました。

元記事はこちらです。
俺のメモ – HAProxy で HTTP ヘッダを操作するメモ(1)〜多段 Proxy 環境でも接続元 IP をオリジナル HTTP ヘッダに挿入する〜