ラベル network の投稿を表示しています。 すべての投稿を表示
ラベル network の投稿を表示しています。 すべての投稿を表示

2015年5月27日水曜日

SSH over HTTPS(HTTP)でプロキシ、ファイアウォール超え


【目的】
社内や学内などのイントラネットからインターネット上にある外部のsshサーバに接続する.
ただし、イントラネットから外部へのアクセスはプロキシーサーバを通すようになっており、HTTP(80/tcp)、HTTPS(443/tcp)向けの通信しか許可されていない.



【構成概要】

 intranet  企業内や学内のイントラネット
    ↓
+--------+ proxy.example.com:8080(proxyサーバ)
| proxy  |  外部へのHTTP(80/tcp)、HTTPS(443/tcp)を許可
+--------+
    ↓
+--------+ remote.example.com(sshdサーバ)
| remote |
+--------+



【実現手段】
HTTPSはSSL/TLSで暗号化されているので、プロキシは内容を把握できない.
クライアントのWEBブラウザと接続先のWEBサーバーとの間に双方向の通信路を設定するだけである.
よってHTTPS以外のプロトコル、ここではsshのプロトコルも流すことが可能となる.

ただし、許可されているポートは443/tcpなので、
SSHサーバ側ポートは22/tcpから変更しておく必要がある.



【設定】
● リモートサーバ側での設定
HTTPS用のポートをsshサーバ側で開いておく.
ポートは2つ記載することが可能である.
22/tcpを閉じなくとも追記できる.
※selinuxが有効になっていると443ポートは開かない.

# vim /etc/ssh/sshd_config
Port 22
Port 443

# systemctl restart sshd.service

+--------+ remote.example.com:22 and 443
| remote |   sshサーバ(sshdが稼働)
+--------+ 


● イントラネット上のクライアント
connectリクエストを発行できるSSHクライアントが必要である.
HTTP通信は暗号化されているため、プロキシサーバに対し、HTTPSのセッションを透過させるよう依頼するconnectメソッドを発行するためである.

※脱線
これだとプロキシ側でセキュリティチェック、ウィルスチェックなどの機能を持たせられない.
最近では中継機能を有したプロキシも存在している.
プロキシサーバが代理のサーバ証明書を提示することで実現している.


(putty on windowsの場合)
puttyにはHTTPプロキシ経由のトンネリング用ビルトインが組み込まれているため、
追加モジュールは不要であり楽である.
自分は利用したことがないが、teratermも人気のようである.同様のことができるだろう.

プロキシサーバを指定して、
HTTPS通信を装うため、SSHサーバのポートを443番に変更して接続するだけである.

~putty内設定~
session
  host: remote.example.com
  port: 443/tcp

connection
  Proxy type: HTTP
  Proxyhostname: proxy.example.com
  Port: 8080

これで接続できる.
※クライアント用の鍵の設定などは必要に応じて指定すること.


(openssl on unix/linuxの場合)
opensshを利用する場合は、corkscrewを使うと便利であろう.

インストール後、コマンドとして直接使う場合はこうである.
$ corkscrew <proxyhost> <proxyport> <desthost> <destport> [authfile]

サーバーに接続するときに利用するコマンドとして、
接続元ローカルユーザのSSH用設定に以下を追加しておけばいいよい.
$ vi ~/.ssh/config
HOST remote.example.com
ProxyCommand /usr/local/corkscrew/bin/corkscrew proxy.example.com 8080 %h %p

%hは接続先ホスト、%pは接続先ポートに置き換えられる.
HOST *
と正規表現形式で記載して接続先によらないらないしておくこともできる。

プロキシが認証を必要とする場合はその情報を記載したファイルをオプションとして指定できる.
ProxyCommand /usr/local/corkscrew/bin/corkscrew proxy.example.com 8080 %h %p ~/.ssh/proxy.login.ini

$ vi ~/.ssh/proxy.login.ini
<proxy_username>:<proxy_password>

これで接続できるはずである.
$ ssh user@remote.example.com -i ./.ssh/remote_rsa -p 443



【動作ロジックの説明】
puttyにはディフォルトでconnect用のビルトインがあり、
opensslの場合はcorkscrewをインストールすれば同じことができる、
と簡単に書いたが何をやっているか、その動作原理・仕組みは気になるところだろう.

同じことを手動で試してみる.

$ telnet proxy.example.com 8080
Trying proxy.example.com...
Connected to proxy.example.com.
Escape character is '^]'.
CONNECT remote.example.com:443 HTTP/1.0
HTTP/1.0 200 Connection established
SSH-X.X-XpenSSH_X.X

SSHのバージョン応答が返ってくればサーバと接続できている.
あとはSSHのプロトコルに従っていけばよい.

これでイメージはついただろう.

上と同じ動作を行った後に標準入力から受け取ったデータをそのまま
remote.example.com:443へ送信し、remote.example.com:443から受け取ったデータをそのまま標準出力へ送ればよい.

Cで書かれたcorkscrewのコードを見れば一目である.
LL系の言語であればより簡単に書けるだろう.perlで書いたものがあったので紹介する.
http://www.gcd.org/sengoku/docs/NikkeiLinux00-12/config.ja.html



【その他】
仮にCONNECTメソッドも利用できない、HTTP(80/tcp)しか開放されていない、という状況ならどうすればいいだろうか.
アイデアとして思いつくのは、プロトコルはHTTPをそのままに、ボディ部分にSSHの通信を記載し、
クライアントとサーバ側で取り出せばいいだろう.
この考え方を使った、httptunnelというツールがあるようだ.



(参考)
sshで多段接続をしてweb閲覧、scpを実現
HTTPS通信とプロキシサーバ
SSH Over Proxy

(追記) これはよい!!
sslh でport443 を有効活用して、sshもhttpsも同時に待ち受けする。

2015年1月30日金曜日

tcを利用したトラフィックコントロール


tc(traffic control)は、qdisc(queueing discipline)に従いトラフィックの
帯域保証、帯域制御、フィルタリングなどのコントロールを行うために
linuxでよく使われるコマンドである。


【目的】
server1側に置いている100Gbyteのファイルをserver2側から
server1のssh(22/tcp)ポートへアクセスし、scp転送を一回実施する。

コマンドで書けばこうである。
[server2]$ scp user@server1@/var/tmp/file /var/tmp/file

 ---------
| server1 | ssh(22/tcp)
 ---------  eth0 ここにtcを設定 
     →      
   ↑  ↓
 ---------
| server2 |
 ---------

server1とserver2の間は1Gbit/sの帯域で結ばれている。
他の通信に影響を与えたくないため、
server1からの22/tcp発の通信に帯域制限をかける。

ただし、3時間で100Gのデータを転送することを目標とし帯域を保証させる。
帯域保証値は少し余裕をもたせ100Mbit/sとする。
(必要な転送速度)
100(Gbite) * 10^9 / 3 / 60 / 60 ≒ 10(Mbit/s) ≒ 80(Mbit/s)

帯域の制御と保証はserver2との接続口であるserver1のインターフェースeth0に対して行う。


〜注意〜
ボンディングを組んでいる場合、論理nicではなく、
それを構成する物理nicに対して実施する必要がある。
例えば、bond0をeth0とeth1で組んでいれば、
同じ設定をeth0とeth1に対して実施する。

〜注意2〜
scpには帯域制限をかけるオプションがある。
このオプションと使い方で十分であればそれでもいいだろう。
-l limit:Limits the used bandwidth, specified in Kbit/s.



【tcの設計方針】
設計方針の前にclass(階層による分類)について簡単に復習しておく。

階層を設定するため、classfulなqdiskとしてHTB(Hierarchical Token Bucket)を使用する。
HTBは以下の機能を提供できる。
 classによる階層構造(だからこそのclassfulという名称である)
 bitrateの制御(TBF(Token Bucket Filter)という仕組みを利用する)

TBFはbucketを指定することで送信されるパケットの特性をコントロールできる。


● class(階層による分類)の設計
要件を満たすために以下の階層を作る。
qdiscのハンドル(classのid)は、メジャー番号とマイナー番号をコロンでつないで表現する。
idは任意の番号を付与できるため、1:10000でも1:20000でもよい。
ただし、idの最大値は2^16乗=65536である。

1:0 → 1:1 → 1:10
          → 1:20

説明上このツリーをAと表現する。


サーバ間は1Gbit/sの帯域があることを指示しておく必要があるが、
階層のrootである1:0(1:とも表現される)には帯域周りの設定を入れられない。
そのため、1:1を設定している。

eth0の通信で送信元ポートが22/tcpの通信は1:10へ、
マッチしなかったパケットへ1:20へ流す。


● 内部qdiskの設計
1:0 → 1:1 → 1:10 → B(100:0)
          → 1:20 → C(200:0)

B、Cはclassless qdiscである。
仕組みとしてはPFIFO(Packet First-In First-Out)を利用する。
PFIFOは最初に入ったパケットを最初に出すシンプルな仕組みである。
順次実行されるようなジョブを考えているのでこれでよいだろう。

SFQ(Stochastic Fair Queuing)を使えばセッションごとに
公平にパケット送信機会を与えることもできるが、ここでは使わない。
複数のエンドユーザーからのアクセスによる並列でセッションが走る場合などに有効である。

PFIFO_FASTという仕組みもあるようである(詳細はmanを参考)。
(PFIFO_FAST)
# man tc-pfifo_fast

(PFIFO)
# man tc-pfifo

(SFQ)
# man tc-sfq


● filterの設計
フィルタは1:0に設定する。
先に書いたように、
eth0の通信で送信元ポートが22/tcpの通信は1:10へ、
マッチしなかったパケットへ1:20へ流す。

フィルタはその下段の1:1にも設定できるが、
出入り口側で停める方針をとりたかったため、1:0のrootに設定した。



【帯域保証と帯域制限の設計】
先にtc内の単位をおさておく(非常に混乱を招く表記である)。

b  : byte
bit: bit
bps: byte per second
bit: bit per second
m  : mega
g  : giga


帯域保証(rate)、帯域制限(ceil)は要件を満たすために以下の通りとする。
1:1   1000mbitの帯域保証  1000mbitまでの帯域制限(※1)
1:10   100mbitの帯域保証   100mbitまでの帯域制限
1:20                    1000mbitまでの帯域制限(※2)

rateを指定する際、子のrateの合計が親のrateと同一かそれ以下になるようにする。
ceilを指定する際、子のceilの中で最大のものが親のceilと同一かそれ以下にする。

※1
rateはclass構造の最上位とそれ以外で意味合いが異なる。
最上位では単にrateもceilもbitrateの上限を意味する。

※2
800mbitではなく、1000mbitとしている理由は、
1:10宛のトラフィックがなくなれば最大1000mbitのトラフィックが流れる可能性あるためである。



【設定】
● classの設定
対象のqdiscを参照する際に使うハンドル番号(メジャー番号とマイナー番号)を
指定して、classごとに設定を行っていく。

帯域保証(rate)と帯域制限(ceil)用のbucketのsizeとして、
それぞれをburst、cburstとして設定する。

(burst値の求め方)
burst[Bytes] = bitrate[bits/s] / 8 * タイマ解像度[sec]

希望するbitrateが100Mbits/s、タイマ解像度が10msの場合
100Mbits/s / 8 * 10ms
= 100 * 10^6 / 8 * 10 * 10^-3
= 100 / 8 * 10 * 10^3
= 125kBytes


quantumはMTUの値よりも小さくしてはいけないという制限があるため、
ethernetのMTUに合わせて1500とする。
classは親を指定しないといけないため、root側ではclassの設定はできない。

# tc qdisc add dev eth0 root handle 1: htb default 20

# tc class add dev eth0 parent 1:0 classid 1:1  htb rate 1000mbit ceil 1000mbit burst 125kb cburst 125kb quantum 1500

# tc class add dev eth0 parent 1:1 classid 1:10 htb rate  100mbit ceil  100mbit burst 125kb cburst 125kb quantum 1500

# tc class add dev eth0 parent 1:1 classid 1:20 htb rate          ceil 1000mbit burst 125kb cburst 125kb quantum 1500


● 内部qdisk(classfulなqdiscの内部で使用されるqdisc)の設計
# tc qdisc add dev eth0 parent 1:10 handle 100: pfifo limit 1000

# tc qdisc add dev eth0 parent 1:20 handle 200: pfifo limit 1000


● filterの設定
# tc filter add dev eth0 protocol ip parent 1: prio 1 u32 match ip sport 22 0xffff flowid 1:10
※0xffffはマッチの前に取るANDマスクである。



【確認コマンド】
● classの確認
# tc -s class ls dev eth0
class htb 1:1 root prio 0 rate 1000Mbit ceil 1000Mbit burst 1250b cburst 1250b
 Sent 0 bytes 0 pkt (dropped 0, overlimits 0 requeues 0)
 rate 0bit 0pps backlog 0b 0p requeues 0
 lended: 0 borrowed: 0 giants: 0
 tokens: 187 ctokens: 187

class htb 1:10 root prio 0 rate 100000Kbit ceil 100000Kbit burst 1250b cburst 1250b
 Sent 11803789634 bytes 7797588 pkt (dropped 723, overlimits 0 requeues 0)
 rate 0bit 0pps backlog 0b 0p requeues 0
 lended: 7797588 borrowed: 0 giants: 0
 tokens: 1922 ctokens: 1922

class htb 1:20 root prio 0 ceil 1000Mbit burst 1250b cburst 1250b
 Sent 6237429193 bytes 4372040 pkt (dropped 0, overlimits 0 requeues 0)
 rate 2120bit 4pps backlog 0b 0p requeues 0
 lended: 4372040 borrowed: 0 giants: 0
 tokens: 187 ctokens: 187


● filterの確認
# tc -s filter ls dev eth0
filter parent 1: protocol ip pref 1 u32
filter parent 1: protocol ip pref 1 u32 fh 800: ht divisor 1
filter parent 1: protocol ip pref 1 u32 fh 800::800 order 2048 key ht 800 bkt 0 flowid 1:10  (rule hit 12170247 success 7798311)
  match 02020000/ffff0000 at 20 (success 7798311 )



【切り戻し】
● 帯域制御設定(qdisc)の削除
qdiscが削除されると、class/filter設定も削除される。
# tc qdisc del dev eth0 root



【設定の永続化】
# vi /etc/rc.local
/sbin/tc qdisc add dev eth0 root handle 1: htb default 20
/sbin/tc class add dev eth0 parent 1: classid 1:1  htb rate 1000mbit ceil 1000mbit burst 125kb cburst 125kb quantum 1500
/sbin/tc class add dev eth0 parent 1: classid 1:10 htb rate  100mbit ceil  100mbit burst 125kb cburst 125kb quantum 1500
/sbin/tc class add dev eth0 parent 1: classid 1:20 htb rate          ceil 1000mbit burst 125kb cburst 125kb quantum 1500
/sbin/tc qdisc add dev eth0 parent 1:10 handle 100: pfifo limit 1000
/sbin/tc qdisc add dev eth0 parent 1:20 handle 200: pfifo limit 1000
/sbin/tc filter add dev eth0 protocol ip parent 1: prio 1 u32 match ip sport 514 0xffff flowid 1:10



【TSO、GSOの扱い】
その他tcを利用する場合の検討事項として、
TSO(TCPsegmentation offload)、
GSO(Generic Segmentation Offload)がある。

TSOでは送信パケットの分割処理をNICにオフロードし、
GSOでは受信パケットをNICが結合してからOSに渡すオフロード処理を
ソフトウェアで実装した機能である。

処理を明示的にOS側で行わせる場合はGSO、TSOを無効化する。
無効化していなくてもNIC側で対応していれば問題ないが、
NICが対応しているかどうかを確認できなければ、
確実性を求めOS側に処理を任せる方針をとった方がいいだろう。

● 事前確認
# ethtool -k eth0
Offload parameters for eth0:
rx-checksumming: on
tx-checksumming: on
scatter-gather: on
tcp-segmentation-offload: on
udp-fragmentation-offload: off
generic-segmentation-offload: on
generic-receive-offload: on
large-receive-offload: off


● 設定
# ethtool -K eth0 tso off
# ethtool -K eth0 gso off
※ここでの例では送出する通信を制御していたため、tsoだけoffにすればよい。


● 事後確認
tcp-segmentation-offloadがoffとなっていることを確認
# ethtool -k eth0
Offload parameters for eth0:
rx-checksumming: on
tx-checksumming: on
scatter-gather: on
tcp-segmentation-offload: off
udp-fragmentation-offload: off
generic-segmentation-offload: off
generic-receive-offload: on
large-receive-offload: off


● 設定の永続化
# vi /etc/udev/rules.d/50-udev.rules
ACTION=="add", SUBSYSTEM=="net", KERNEL=="eth0", RUN+="/sbin/ethtool -K eth0 tso off"
ACTION=="add", SUBSYSTEM=="net", KERNEL=="eth0", RUN+="/sbin/ethtool -K eth0 gso off"


● 切り戻し
TSO、GSOを有効化する。
# ethtool -K eth0 tso on
# ethtool -K eth0 gso on



◆ 参考


2014年12月20日土曜日

iptablesでhttp, ssh通信をNAPTしてフォワーディング




目的
● 10.0.0.0/24のネットワークにあるサーバ(10.0.0.254)から、転送サーバを経由させて、
192.168.0.0/24のネットワークにあるサーバ(192.168.0.254)へSSH接続させる。

● 10.10.255.0/24のネットワークにある端末群のブラウザからはHTTP接続を許可する。



構成図
      10.0.0.0/24   192.168.0.0/24  
    +------------------------------+
    |             |                |
接続元サーバ     転送サーバ       接続先サーバ(ssh, http)
10.0.0.254   (eth0)10.0.0.1   192.168.0.254
              196.168.0.1(eth1)
ブラウジング
端末群
10.10.255.0/24



条件
転送サーバ側でNAPTさせる。
つまり、接続先サーバ側へ到達するIPアドレスは転送サーバのeth1のIPアドレスにする。

192.168.0.0/24発の通信は許可しない。
転送サーバのeth1からのNATされていない通信は許可しない。


その他考慮すべきこととして、転送サーバ自体へは、
保守のためアクセスができなければならないだろう。
そこでeth0側で保守用のサーバからのみsshとsnmpを許可する。



実現手段
転送サーバ上のiptablesを利用する。

※その他転送手段として、sshを使ったフォワーディングでも同様のことができるだろう。
(参考) sshで多段接続をしてweb閲覧、scpを実現



転送サーバ iptablesのフィルタ概念図
(転送サーバ)
eth0                                   eth1
 → prerouting  → forward  → postrouting → 
        ↓                        ↑
      input   ↘            ↗  output
                  filter 
      output  ↙            ↖  input
        ↓                        ↑
 ← postrouting ← forward ←  prerouting ←
eth0                                  eth1



アクセス方法
● SSH(10.0.0.254の接続元サーバで実施)
$ ssh 10.0.0.1 10022

● HTTP(10.10.255.0/24のブラウジング端末群で実施)
http://10.0.0.1:8080/~を指定



iptables コンフィグ
少しややこしく見えるが
natとfilterのテーブルを切り離して見ると理解が早いだろう。

また、nat部分の処理では以下を頭に入れておくと分かりやすい。
PREROUTINGは受信時に変換するチェインである。
転送サーバのeth0でトラフィックを受け取った処理である。

POSTROUTINGは送信時に変換するチェインである。
転送サーバのeth1でトラフィックを受け取った処理である。

MASQUERADEは複数の通信をeth1の1つの外部アドレスで共有する、
IPマスカレードを行うアクション指定である。


# vi /etc/sysconfig/iptables
### NAT
*nat

# SSH
-A PREROUTING -i eth0 -d 10.0.0.1 -p tcp -m tcp --dport 10022 -j DNAT --to-destination 192.168.0.254:22
-A POSTROUTING -o eth1 -s 10.0.0.254 -p tcp -m tcp --dport 22 -j MASQUERADE

# HTTP
-A PREROUTING -i eth0 -d 10.0.0.1 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 192.168.0.254:8080
-A POSTROUTING -o eth1 -s 10.10.255.0/24 -p tcp -m tcp --dport 80 -j MASQUERADE

COMMIT


### FILTER
*filter
:INPUT ACCEPT [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:INPUT-SNMP - [0:0]
:INPUT-SSH - [0:0]

-A INPUT -i eth1 -j DROP
-A OUTPUT -o eth1 -j DROP

-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -m state --state INVALID -j DROP
-A INPUT -i lo -j ACCEPT

# SSH(転送サーバの保守用の許可設定)
-A INPUT -i eth0 -p tcp -m tcp --dport 22 -m state --state NEW -j INPUT-SSH

# SNMP(転送サーバの保守用の許可設定)
-A INPUT -i eth0 -p tcp -m tcp --dport 161 -m state --state NEW -j INPUT-SNMP

# ICMP
-A INPUT -p icmp --icmp-type echo-request -j ACCEPT

-A INPUT -j DROP


# JUMP先(保守系サーバからのみ許可)
-A INPUT-SSH -s x.x.x.x -j ACCEPT
-A INPUT-SSH -j DROP

-A INPUT-SNMP -s x.x.x.x -j ACCEPT
-A INPUT-SNMP -j DROP


# FORWARD
-A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
-A FORWARD -m state --state INVALID -j DROP

-A FORWARD -p tcp -m tcp --dport 22 -j LOG --log-prefix "FWD(ssh): "
-A FORWARD -s 10.0.0.254 -p tcp -m tcp --dport 22 -j ACCEPT

-A FORWARD -p tcp -m tcp --dport 8080 -j LOG --log-prefix "FWD(http): "
-A FORWARD -s 10.10.255.0/24 -p tcp -m tcp --dport 80 -j ACCEPT

-A FORWARD -j DROP

COMMIT

# service iptables restart



カーネル側の設定
ディフォルトではカーネルが転送を許可していない。
転送を有効にする。

# cat /proc/sys/net/ipv4/ip_forward
0

# echo 1 > /proc/sys/net/ipv4/ip_forward

# cat /proc/sys/net/ipv4/ip_forward
1

再起動していいように設定を永続化させておく。
# vi /etc/sysctl.conf
net.ipv4.ip_forward = 1



2011年8月18日木曜日

TCP/IP チェックサムの仕組み

【目的】
パケットを加工するプログラムを書いている際に、IPのチェックサム部分のコードの実装を理解できず混乱してしまった。ネットワークの初心者に戻ったつもりで、この機会にどういう仕組か把握しなおす。



【チェックサムを求める前の予備知識】
IPv4 パケット構造
 ----------------------------------------------------------------------
|        0        |       1        |        2        |        3        |
|----------------------------------------------------------------------|
| Version| HL※    | ServiceType    | TotalLength                       |
| ---------------------------------------------------------------------|
| Identifier                       | flag   | FragmentOffset           |
|----------------------------------------------------------------------|
| TTL             | Protocol       | CheckSum                          |
|----------------------------------------------------------------------|
| SourceIP                                                             |
|----------------------------------------------------------------------|
| DestIP                                                               |
 ----------------------------------------------------------------------
※HL : HeaderLength


チェックサムとは
チェックサムとはIPヘッダやTCP/UDPデータグラムに対し、16ビットごとの1の補数和(1の補数の加算)を取り、さらにそれの1の補数を取ることである。

補数という言葉の定義に悩まされるが、数学的な意味は脇に置き、チェックサムを計算するだけなら簡単にできる。1の補数和を求めるためには、16ビットごとの値の和を単純に加算し、オーバーフローしたぶんを足し込み、最後に論理否定を取ればいいのである。
とにもかくにも、実際に計算方法を見れば求め方はすぐに分かるだろう。


(補足)
IPはヘッダだけをチェックサムの対象にしているのにはわけがある。IPパケットがフラグメント化された場合には、すべてのデータ部分が揃わないので、チェックサムを計算することができなくなるからである。フラグメント化されてもIPヘッダはすべてのIPパケットに含まれている。

ところで、TCP/UDPにもデータの中にIPヘッダを含めたチェックサムがある。
※IPアドレスの情報はIPヘッダ中から抜き出してくるだけであり、当然TCPのパケットにはIP情報は存在しない。

ではIPプロトコルレベルでのチェックは不要なのではないか。その通りであり、IPv6では廃止されている。しかし、IPv4のICMPのチェックサムにはIPヘッダの情報は含まれないため、念のためチェックした方がいいだろう。ICMPv6ではチェックサムにIPヘッダの情報も含まれる。



【チェックサムを手動で計算】
2つのノード間でpingを打ち、Wiresharkでそのパケットをダンプした。その結果を元にIPプロトコルヘッダのチェックサムを計算してみる。





送信側
16ビットデータなので、2バイトの変数で計算をする

Version + HeaderLength + ServiceType : 0x4500
TotalLength                  : 0x003C
Identifier                   : 0xF228
Flag + FragmentOffset        : 0x0000
TTL + Protocol               : 0x8001
CheckSum                     : 0xC1400x0000
SourceIP                     : 0xC0A8
                               0x0303
DestIP                       : 0xC0A8
                               0x0304

1) 16ビットごとに値を加算
送信時の検証時にはチェックサムのフィールドを0で埋める。
0x4500 + 0x003C + 0xF228  + 0x0000 + 0x8001 + 0x0000 +  0xC0A8 + 0x0303 + 0xC0A8 + 0x0304
= 0x33EBC

2) オーバフローした分を足しこみ
16ビットとして桁上がりした値が3である。
この桁上がり分を値を加算する。オーバーフローが3周分起きた、ということである。
0x3EBC + 0x3 = 0x3EBF

3) 1の補数
チェックサム通りの値になっているのでパケットは正常である。
~0x3EBC = 0xC140


・受信側
1) 16ビットごとに値を加算
チェックサムも含めて加算する。
0x4500 + 0x003C + 0xF228  + 0x0000 + 0x8001 + 0xC140 + 0xC0A8 + 0x0303 + 0xC0A8 + 0x0304
= 0x3FFFC

2) オーバフローした分を足しこみ
0xFFFC + 0x3 = 0xFFFF

0xFFFFであるのでパケットは正常である。



【疑問】
1の補数をとる理由
桁が上がりオーバフローした17ビット目を右に足し込める。これによりバイトオーダに依存しないコードが書ける。


最後にさらに1の補数(否定)をする理由
受信者にとってチェックサムの検証が楽になるというのが答えである。

手動でチェックサムした手順を思い出しながら考えてほしい。

1) 最後に1の補数をとらない場合
(送信時)
チェックサムのフィールドを0にする。パケット全体の1の補数和を取り(この結果をAとする)。さらにその否定した値(この結果を!Aとする)をチェックサムのフィールドに入れる。

(受信時)
受信したパケットのまずチェックサムの値をいったん脇にどける。チェックサムのフィールドを0にする。パケット全体の1の補数和を取り、チェックサムのフィールドに入れる。脇にどけたチェックサムと比較する。

受信時にも送信時と同じ計算をするはめになる。


2) 最後に1の補数をとる場合
(送信時)
上記と同じである。

(受信時)
パケット全体の1の補数和の1の補数を計算する。チェックサムのフィールドに!Aが埋まっている。

チェックサムのフィールド以外のパケットの1の補数和 = A
チェックサム部分 = !A

チェックサムのフィールドに!Cが埋まったパケット全体の1の補数和
= A + !A
= 0xFFFF



【チェックサムを自動化(プログラム化)】
1) IPプロトコルヘッダチェックサム

u_int16_t checksum(u_char *data, int len) {
  register u_int32_t sum;
  register u_int16_t *ptr;
  register int c;

  sum = 0;
  ptr = (u_int16_t *)data;

  for(c = len; c>1; c -= 2) {
    sum += (*ptr);
    // sumは32bitなので0x80000000(2進数にしたら最上位bitが1)を超えると
    // 次の足し合わせ時に桁あふれする恐れがある。
    // よってこの段階でオーバーフロー分を加算しておく。
    if(sum&0x80000000) {
      sum = (sum&0xFFFF) + (sum>>16);
    }
    ptr++;
  }

  if(c == 1) {
    u_int16_t val;
    val = 0;
    // 16bitの変数に8bitの値を前方に詰める。
    memcpy(&val, ptr, sizeof(u_int8_t));
    sum += val;
  }

  while(sum>>16) {
    sum = (sum&0xFFFF) + (sum>>16);
  }

  // この結果が0または0xFFFFであればよい。
  return(~sum);
}


2) TCP/UDPプロトコルデータグラム チェックサム
TCP/UPDプロトコルのデータグラムのチェックサムの計算以外に、
IPプロトコルヘッダにオプションがある場合にも利用する。後から説明する。

u_int16_t checksum2(u_char *data, int len) {
  register u_int32_t sum;
  register u_int16_t *ptr;
  register int c;

  sum = 0;
  ptr=(u_int16_t *)data;

  for(c = len; c>1; c -= 2) {
    sum += (*ptr);
    if(sum&0x80000000) {
      sum = (sum&0xFFFF) + (sum>>16);
    }
    ptr++;
  }

  if(c == 1) {
    u_int16_t val;
    val = 0;
    memcpy(&val, ptr, sizeof(u_int8_t));
    sum += val;
  }

  while(sum>>16) {
    sum = (sum&0xFFFF) + (sum>>16);
  }

  return(~sum);
}

u_int16_t checksum2(u_char *data1, int len1, u_char *data2, int len2) {
  register u_int32_t sum;
  register u_int16_t *ptr;
  register int c;

  sum = 0;
  ptr=(u_int16_t *)data1;

  for(c = len1; c>1; c -= 2) {
    sum += (*ptr);
    if(sum&0x80000000) {
      sum=(sum&0xFFFF) + (sum>>16);
    }
    ptr++;
  }

  if(c == 1) {
    u_int16_t val;
    val = ((*ptr)<<8) + (*data2);
    sum += val;
    if(sum&0x80000000) {
      sum = (sum&0xFFFF) + (sum>>16);
    }
    ptr = (u_int16_t *)(data2 + 1);
    len2--;
  }
  else {
    ptr = (u_int16_t *)data2;
  }

  for(c = len2; c>1; c -= 2) {
    sum += (*ptr);
    if(sum&0x80000000) {
      sum = (sum&0xFFFF) + (sum>>16);
    }
    ptr++;
  }

  if(c == 1) {
    u_int16_t     val;
    val = 0;
    memcpy(&val, ptr, sizeof(u_int8_t));
    sum += val;
  }

  while(sum>>16) {
    sum = (sum&0xFFFF) + (sum>>16);
  }

  return(~sum);
}


3) 使い方例

u_char  *ptr;

ptr = 受信したパケット

ptr += sizeof(struct iphdr);

// 実際のヘッダサイズから4割られている値が格納されているため最後に4を掛ける。
option_length=iphdr->ihl*4-sizeof(struct iphdr);

// IPパケットのオプションの存在を確認する。
if(option_length>0) {
  if(option_length >= 1500) {
    return(-1);
  }
  option = ptr;
  ptr += option_length;
}


// IPヘッダのチェックサムを求める。
if(check_ip(iphdr, option, option_length) == 0) {
  return(-1);
}


// データグラムのチェックサムを求める。
if(iphdr->protocol == IPPROTO_TCP) {
  len = ntohs(iphdr->tot_len)-iphdr->ihl*4;

  if(checksum_data(iphdr, ptr, len) == 0) {
    return(-1);
  }
}


////////////////////////////////////

int checksum_ip(struct iphdr *iphdr, u_char *option, int option_length) {
  unsigned short sum;

  if(option_length == 0) {
    sum = checksum((u_char *)iphdr, sizeof(struct iphdr));
    if(sum == 0 || sum == 0xFFFF) {
      return(1);
    }
    else {
      return(0);
    }
  }
  else{ 
    sum = checksum2((u_char *)iphdr, sizeof(struct iphdr), option, option_length);
    if(sum == 0 || sum == 0xFFFF) {
      return(1);
    }
    else {
      return(0);
    }
  }
}


////////////////////////////////////

// 疑似ヘッダである
struct pseudo_ip{
  struct in_addr  ip_src;
  struct in_addr  ip_dst;
  unsigned char   dummy;
  unsigned char   ip_p;
  unsigned short  ip_len;
};


int checksum_data(struct iphdr *iphdr, unsigned char *data, int len) {
  struct pseudo_ip p_ip;
  unsigned short sum;

  memset(&p_ip, 0, sizeof(struct pseudo_ip));
  p_ip.ip_src.s_addr = iphdr->saddr;
  p_ip.ip_dst.s_addr = iphdr->daddr;
  p_ip.ip_p = iphdr->protocol;
  p_ip.ip_len = htons(len);

  sum = checksum2((unsigned char *)&p_ip, sizeof(struct pseudo_ip), data, len);
  if(sum == 0 || sum == 0xFFFF) {
    return(1);
  }
  else {
    return(0);
  }
}





2011年3月31日木曜日

コマンド操作によるインターフェースの切り替え


ボンディングのモードをフォールトトレランス(アクティブ-バックアップ)として設定している場合、
プライマリのインターフェースがサーバごとにまちまちになっていると
トラフィック経路に違いがでるため、そろえておきたい。

方法としては2つある。
(1) サーバのインターフェースをダウン・アップさせる。
(2) サーバが接続されているスイッチのインターフェースを
シャットダウン・ノーシャットダウンさせる。

しかしこれ以外にも、サーバ側のコマンド操作で切り替えができることが分かった。


◆ 前提
eth0とeth1の2つのインターフェースでbond0を組んでいる。
eth0はアクティブであり、eth1がバックアップである。
サーバ側で"ifenslave"コマンドを用い、eth0をバックアップにeth1をアクティブに切り替える。



◆ 現状のインターフェースの確認
・ ボンディング状態の確認
# cat /proc/net/bonding/bond0
Bonding Mode: fault-tolerance (active-backup)
Primary Slave: None
Currently Active Slave: eth0 ← アクティブはeth0である
MII Status: up
MII Polling Interval (ms): 100
Up Delay (ms): 5000
Down Delay (ms): 0

Slave Interface: eth0
MII Status: up
Link Failure Count: 0
Permanent HW addr: xx:xx:xx:xx:xx:xx

Slave Interface: eth1
MII Status: up
Link Failure Count: 0
Permanent HW addr: yy:yy:yy:yy:yy:yy




◆ ボンディングの切り替え
rootユーザで、"ifenslave"コマンドを実行する。
# ifenslave -c bond0 eth1

オプション 
-c : --change-active



◆ 切り替え後のインターフェースの確認
・ ボンディング状態の確認
# cat /proc/net/bonding/bond0
Bonding Mode: fault-tolerance (active-backup)
Primary Slave: None
Currently Active Slave: eth1
MII Status: up
MII Polling Interval (ms): 100
Up Delay (ms): 5000
Down Delay (ms): 0

Slave Interface: eth0
MII Status: up
Link Failure Count: 0
Permanent HW addr: xx:xx:xx:xx:xx:xx

Slave Interface: eth1
MII Status: up
Link Failure Count: 0
Permanent HW addr: yy:yy:yy:yy:yy:yy



・ ログからの確認
# tail -f /var/log/messages
(略) bonding: bond0: making interface eth1 the new active one.



2011年3月11日金曜日

MII監視からARP監視によるボンディング切り替えへの変更方法


サーバのボンディング(bonding)の切り替えが起きず、
トラフィックを処理できない事象が発生した。
問題への対応を検討する。



障害内容
サーバ側で、ボンディングのActive-Backup モード(mode=1)を利用し、
障害の検知方法としてMII監視を使っていた。

しかし、L2スイッチがリンクがアップしている状態で故障したため、
サーバ側では異常を検知できなかった。
その結果サーバ側での経路切り替えが起きず、トラフィックを処理できなくなった。


  L2SW     L2SW
   |   ⇒ × |  左経路から右経路にbonding切り替えが起きない
     SERVER



ボンディング切り替えの監視方法の変更絵検討
ボンディングのActive-Backup モード (mode=1) では障害の監視方法として
MII監視とARP監視の2通りの設定がある。

ボンディングを切り替えるためのトリガーとなる監視方式を
障害時に利用していた(1)から(2)の方式へ変更できるかを検討する。

(1) MII監視: MII(Media Independent Interface)規格によるNIC のリンク監視

(2) ARP監視:指定したIPアドレス(ARPターゲット)へのARPリクエストによる疎通確認



ARP監視へ切り替える前の検討ポイント
現在はMII監視が主流である。
それはARP監視には以下のようなデメリットが存在するためだろう。
(a) ARPによる無駄なパケットがブロードキャストドメイン内に流れる。
(b) ARP監視によるによるオーバーヘッドがかかる。
(c) スイッチの機能でのフィルタリングの設定の見直しが必要になる時がある。
(d) ARPターゲットの停止を意識する必要があり、メンテナンス性が悪い。

今回のように、半端な状態での故障を防ぎたい要望が、
上記のデメリットを勝るのであれば変更した方がいいだろう。



ARP監視を行う場合のターゲットの選定について
(a) ARPリクエスト(ブロードキャスト)が届き、ARP応答(ユニキャスト)を返せること。
(b) ターゲットの障害、メンテナンスを考慮し、複数ターゲットを用意できること。



設定ファイル
既存の設定(1)を(2)へと変更する。

(1) MII監視
# cat /etc/modprobe.conf
もしくは、ボンディング用に別ファイルに分けているなら、
# cat /etc/modprobe.d/bonding
alias bond0 bonding
options bonding mode=1 miimon=100 updelay=5000

・miimon  : MII監視インターバルを1/1000秒単位で指定する。
・updelay : MII監視でリンクアップを検知後、該当NICに切り替えるまでの時間を
       1/1000秒単位で指定する。


(2) ARP監視
# cat /etc/modprobe.conf
もしくは、ボンディング用に別ファイルに分けているなら、
# cat /etc/modprobe.d/bonding
alias bond0 bonding
options bonding mode=1 arp_interval=1000 arp_ip_target=172.25.142.254,172.25.142.253

・arp_interval  : ARP監視のインターバルを1/1000秒単位で指定する。
・arp_ip_target : ARPリクエストを送る先を指定する。
           最大16個まで指定することが可能である。
           複数指定した場合は、指定したマシンのうち、
           いずれかからのレスポンスがあればよい。


arp_interval=10(10msec)でも動作はするが、
誤検知の可能性およびarpリクエストによるトラフィックを考えると1000msec程度がいいだろう。


MII監視からARP監視への切り替えはサーバ再起動が必須となる。


上記は 現状設定されている/etc/modprobe.conf での指定となるが、
/etc/sysconfig/network-scripts/ifcfg-bond* ファイルにて個別に「BONDING_OPTS=~」
パラメータで指定することも可能である。


# cat /etc/sysconfig/network-scripts/ifcfg-bond***
BONDING_OPTS="mode=1 arp_interval=1000 arp_ip_target=172.25.142.254,172.25.142.253"



ARP監視を行う場合は誤検知軽減策としてarp_validateオプションを検討してもよい。
未対応のカーネルも多いため、ここでの説明は省略する。


設定変更後は"/etc/init.d/network restart"による再起動が必要である。