Linux 上多条对外连线(Multi-Path)实作------带宽负载平衡

原文地址 (http://www.study-area.org/tips/multipath.htm)
version 0.04
date 2004-09-27


一、前言

由于ADSL 及Cable Modem 的普及,越来越多企业或个人所拥有的对外连线早已超过一条了,
不管是固接还是拨接,相信不少人都在思考如何用最有效的方式将多条连线作整合,
将所有连线的使用发挥至最大极限及最佳的使用率。
除了起到负载的分摊之外,还能达到断线备援目的,以提供更灵活的连线整合方案。

本文将以实作的方式试图在Linux 上做到上述要求。


二、环境

我先说明一下我的测试环境:

2.1 系统方面:
我目前测试的系统是RedHat 9.0 Linux ,采用"伺服器"类型安装,并没提供X 界面。
透过APT 更新至最新修补,并没安装distro 之外的套件。
事实上,只要实作所需的套件满足的话,并不需要安装任何伺服器套件。
甚至,核心版本也不需作任何修改,只是某些连线特征会有些差异(后文[7.1]再详述)。

2.2 网路方面:
原本的seednet adsl 是五个IP 拨接的, 除了之前透过ip share 来使用外,我再起了另外一个ppp0 界面。 
此外另外再牵了一条hinet 固一adsl 。
如此环境,基本上能够分别测试到如下这几种连线方式: 
* 固接(固定IP) 
* 拨接(非固定IP) 
* ip share(非固定IP) 
见图: 


                  /\__/\__/\ 
              ,--| internet |--. 
             / \/--\/--\/ \ 
            | | 
            | | 
+--[seednet ADSL] [hinet ADSL] 
| (非固定) (固一) 
| | 220.130.96.254 
| [ ip share ] | 
| 192.168.100.1 | 
| | | 
| 192.168.100.2 220.130.96.21 
| +----------------------------------+ 
+---| (eth1) (eth0) | 
ppp0| kernel 2.4.23 | 
    +----------------------------------+ 
            (my linux box RH9.0) 
 
2.3 测试方式
虽然,拨接adsl 都是同一设备,或许还不十分理想,目前也只能如此了... 
我采用的是拔线的方式来测断线的,暂还没想到其他方式,或许大家可以帮忙想想的... 
我的测试基本上是用ping 来做: 
* 若是ppp 或不指定测试目标,我会用next hop 来测。 
* 否则,我会用traceroute 找出ISP 端机房的router ip , 
  而不用next hop ,是因为怕不准,比方说断线是外部线路之类的。 

若大家有更好提议,也欢迎提供....


三、设计目标

实作方案基本上要做到如下这些要求:

3.1 出向负载分流
所有连线在正常情况下,将共同分摊由内对外产生的连线流量。
对于固定连线,可指定权重(weight),否则使用相同权重( weight 1)。

3.2 断线侦测
若有连线断掉,需自动从路由中移除。
当连线恢复时,则自动增加路由。

3.3 进向负载分流(额外需求)
外部连线进来,尽可能的将流量分摊在每一条连线上。


四、设计构思

4.1 出向负载分流
利用Linux iproute 程式,将每一条连线的gateway 及其权重增加至路由表的default route 中。
其中的固接adsl 及ip share 连线,需静态指定其gateway 位址及权重。
其余拨接或非固接连线,则用iproute 程式抓出当时的gateway ,权重分配为1 。

4.2 断线侦测
不管是固接还是非固接连线,用手工方式(如traceroute)抓出ISP 端的机房router 位址,
并确定用ping 可以获得回应。
然后用静态路由指定通向router 的nexthop gateway ,定期使用ping 来侦察连线。
每次侦测后都重跑iproute 程式,并重新设定路由表。
若ping 不成功,则抓出连线界面,且在重设路由表时忽略该界面连线。

4.3 进向负载分流(额外需求)
使用动态dns(ddns) 将为每一界面位址分配一个A 记录,以达到轮询回应结果,
从而让不同的外部连线请求轮流使用每一条连线进入。

本实作方案中,ddns server 建议部署于外部的稳定连线的主机上。
关于动态dns 的server 设定,不含在本次实作方案之内。可请参考:
	http://www.study-area.org/tips/ddns.htm

设计难点在于ipshare :
	其中ipshare 部份需另行设定nat 转线。
	否则需从清单中移除。
	
	若nat 设定正确,接下来的难点是获得ipshare 的IP (因为也是非固接的),
	这需要在外部的web server 另行开发php 程式来获取(可置于ddns server)。
	关于这部份设计,请参考:
		http://phorum.study-area.org/viewtopic.php?p=108638
	或使用如下代码:
----------------------- cut here ----------------------
<?php 

//Get the real client IP ("bullet-proof"???) 

function GetProxyIP() 
{ 
       if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"), "unknown")) 
           $ip = getenv("REMOTE_ADDR"); 
       else 
           $ip = "unknown"; 
   return($ip); 
}/*-------GetIP()-------*/ 


function GetClientIP() 
{ 
   if (getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"), "unknown")) 
           $ip = getenv("HTTP_CLIENT_IP"); 
       else if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"), "unknown")) 
           $ip = getenv("HTTP_X_FORWARDED_FOR"); 
       else if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"), "unknown")) 
           $ip = getenv("REMOTE_ADDR"); 
       else if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], "unknown")) 
           $ip = $_SERVER['REMOTE_ADDR']; 
       else 
           $ip = "unknown"; 
   return($ip); 
} 

printf("proxy IP: "); 
print_r(GetProxyIP()); 
printf("<br>\n"); 
printf("client IP: "); 
print_r(GetClientIP()); 

?>
----------------------- cut here ----------------------


五、实作指令

5.1 获取当前各界面之ip :
# ip address show
1: lo: <LOOPBACK,UP> mtu 16436 qdisc noqueue
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 brd 127.255.255.255 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000
    link/ether 00:02:44:84:26:4f brd ff:ff:ff:ff:ff:ff
    inet 220.130.96.21/24 brd 220.130.96.255 scope global eth0
3: eth1: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000
    link/ether 00:20:ed:36:f9:74 brd ff:ff:ff:ff:ff:ff
    inet 192.168.100.2/24 brd 192.168.100.255 scope global eth1
4: eth2: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast qlen 1000
    link/ether 00:02:b3:4b:69:49 brd ff:ff:ff:ff:ff:ff
    inet 10.1.2.3/24 brd 10.1.2.255 scope global eth2
15: ppp0: <POINTOPOINT,MULTICAST,NOARP,UP> mtu 1492 qdisc pfifo_fast qlen 3
    link/ppp
    inet 210.64.33.27 peer 210.64.33.1/32 scope global ppp0

5.2 设定ip rule :
# ip rule add pref 10 from 220.130.96.21 table 10
# ip rule add pref 20 from 192.168.100.2 table 20
# ip rule add pref 30 from 210.64.33.27 table 30

5.3 设定ip route 各table :
# ip route replace default via 220.130.96.254 dev eth0 table 10
# ip route replace default via 192.168.100.1 dev eth1 table 20
# ip route replace default via 210.64.33.1 dev ppp0 table 30

5.4 设定ip route main table:
# ip route replace default \
> nexthop via 220.130.96.254 dev eth0 weight 4 \
> nexthop via 192.168.100.1 dev eth1 weight 1 \
> nexthop via 210.64.33.1 dev ppp0 weight 1

5.5 检视main table 规则:
# ip route show
210.64.33.1 dev ppp0 proto kernel scope link src 210.64.33.27
192.168.100.0/24 dev eth1 scope link
220.130.96.0/24 dev eth0 scope link
10.1.2.0/24 dev eth2 scope link
169.254.0.0/16 dev eth2 scope link
127.0.0.0/8 dev lo scope link
default
        nexthop via 220.130.96.254 dev eth0 weight 4
        nexthop via 192.168.100.1 dev eth1 weight 1
        nexthop via 210.64.33.1 dev ppp0 weight 1

5.6 刷新route cache:
# ip route flush cache

5.7 测试及确认连线生效:
基本上,若在输入上述命令中没遇到error ,那设定就已完成。
接下来可起用多个对外连线(或用ping),
然后使用tcpdump -i any 来查看封包是否能分摊在每一条连线上。


六、撰写script :
若前述设定经测试成功后,接下来就是撰写script 以让工作自动进行。

6.1 configs

* 说明:
提供给其它scripts 所需的共同变数值。

* 代码:
----------------------- cut here ----------------------
BASH=/bin/bash
MAIL=root
SH=/bin/bash
LS=/bin/ls
GREP=/bin/grep
AWK=/bin/awk
SED=/bin/sed
HEAD=/usr/bin/head
TAIL=/usr/bin/tail
CUT=/bin/cut
CAT=/bin/cat
WC=/usr/bin/wc
TR=/usr/bin/tr
SEQ=/usr/bin/seq
SORT=/bin/sort
LSMOD=/sbin/lsmod
IP=/sbin/ip
PING=/bin/ping
IPTABLES=/sbin/iptables
ROUTE=/sbin/route
DATE=/bin/date
DHCP_DIR=/var/lib/dh​​cp

STATIC=./statics


INTERVAL=60 # check time interval in seconds
RUN_SCRIPT=./run_ip.sh # script to run ip route
DDNS_SCRIPT=./ddns/ddns.sh # script to run ddns update
DDNS=yes # enable/disable ddns update

#-- define public destination for ping --#
fixed_dest="168.95.1.1"

#-- define fixed interface & gateway --#
#-- format: "if:gw:weight [if:gw:weight]..." --#
fixed_gw="eth0:220.130.96.254:4 eth1:192.168.100.1:1"
fixed_if="$(echo -e "${fixed_gw// /\n}" | cut -d: -f1)"

#-- define internal interface, or comment out for none --#
int_if="eth0"

#-- statict routing dest & order --#
#-- format: "static_xxx=value; static_xxxx_order=value"
static_seed_net="139.175.0.0/16"
static_seed_net_order="eth1 eth0"
static_studyarea=140.113.27.184
static_studyarea_order="eth0 eth1"

----------------------- cut here ----------------------

* 变数设定说明:
	fixed_dest=
		用来作ping 测试的外部IP。
	fixed_gw=
		固定界面之闸道及权重,格式为"界面名称:闸道位址:权重"。
	int_if=
		内部界面名称,可设定多个。该界面将不会用来设定default route 。
	INTERVAL=
		侦测动作之时间间隔,以秒数作单位。
	RUN_SCRIPT=
		执行路由设定script 之路径。执行时可使用-d 选项排除不必要之界面。
	DDNS_SCRIPT=
		执行动态DNS 更新script 之路径。执行时可使用-e 选项排除不必要之界面。
	DDNS=
		启用或关闭ddns 更新(yes|no)。
	static_xxxxxx=
		静态路由之目的地,可为单一IP 或net_ID/mask 。xxxxxx 为任意名称。
	static_xxxxxx_order=
		静态路由选取的界面次序。左边为最优,若该界面失效,则选下一个。



6.2 run_ip.sh

* 说明:
此script 用来抓出系统全部界面及nexthop gateway ,
并完成路由规则及路由表设定。

* 代码:
----------------------- cut here ----------------------
#!/bin/bash
#
# script name: run_ip.sh
# purpose: changing route table.
# author: netman(netman@study-area.org)
# license: GPL(http://www.gnu.org/licenses/gpl.html)
#
# date: 2004-09-23
# version: v0.05
#
# caveate:
# 1) tested on redhat 9.0 linux only
# 2) iproute program is required
#
#------------------------------------------------- -----------
# change log:
# 1) 2004-03-23​​ v0.01
# * first beta release
# 2) 2004-03-24 v0.02
# * BUGFIXED:
# - Add IPIF and GWIP checking before change route.
# 3) 2004-04-15 v0.03
# * Add function:
# - Restart network if dead device detected.
# 4) 2004-07-07 v0.04
# * IMPROVEMENT
# - Create central config file
# * Add function:
# - Enable static routing
# 5) 2004-09-23 v0.05
# * IMPROVEMENT
# - More accurate on deleting old rules
# - More accurate on deleting dead interface
# - Add gw determining for dhcp interface
# - Add deletion for dead route
# * BugFix
# - Use central conf file
# - Filter out IPv6 address
#
#------------------------------------------------- -----------


WD=${0%/*}
CONFIG=$WD/configs
[ -f $CONFIG ] && . $CONFIG || {
        echo "${0#*/}: ERROR: can not load $CONFIG."
        exit 1
}

#-- route command prefix --#
DEFGW='/sbin/ip route replace default'

#-- define dead interface --#
while getopts "d:" opt; do
        case $opt in
                d) dead_dev=$OPTARG ;;
        esac
done

#-- determine active interfaces --#
interfaces=$($IP address | $GREP -E 'ppp|eth' | \
	$AWK -F': ' '/^[0-9][0-9]*:/{print $2}' | $SORT -u \
	| grep -Ev "${int_if// /|}")

#-- remove old rule table --#
for i in $interfaces; do
	$IP address | $GREP -A2 $i | $AWK '/inet[^6]/{print $2}' | $CUT -d'/' -f1
done | xargs -n1 echo from | while read line; do
	$IP rule | grep "$line"
done | $SED -e "s/^/ip rule del pref /;s/://;s/lookup/table/" | $BASH
unset i


#-- remove dead interfaces --#
if [ -n "​​$dead_dev" ]; then
        interfaces=$(echo "$interfaces" | grep -Ev "${dead_dev// /|}")
	for dev in $dead_dev; do
		DEF_ROUTE=$($IP route list | grep '^default' | grep "dev $dev")
		echo "$DEF_ROUTE" | while read line; do
			$IP route del $line
		done
	done
fi

#-- exit while all dead --#
[ "$interfaces" ] || {
	$IP route del default
	exit 2
}


#-- define table_id --#
init_num=10
offset=`echo -e "${interfaces// /\n}" | $WC -l`
last_num=$(($init_num + $((offset * 10)) ))
tb_num=`$SEQ -s ' ' $init_num 10 $last_num`

#-- FUNCTION: determine DHCP assigning gw --#
dhgw() {
    	get_lastest () {
		OIFS="$IFS"
		IFS='
'	
		for line in $RnT1 $RNT2; do
			l="$(echo $line | grep -o 'lease-time[^;]*' | cut -d' ' -f2)"
			r="$(echo $line | grep -o 'routers[^;]*' | cut -d' ' -f2)"
			e="$(echo $line | grep -o 'expire[^;]*' | cut -d' ' -f3-)"
			EX_TIME=$($DATE -d "$e" +%s)
			if [ "$((EX_TIME - l))" -gt "${P_TIME-0}" ]; then
				P_TIME=$((EX_TIME-l))
				D_TIME=$EX_TIME
				ROUTER=$r
			fi
		done
		IFS="$OIFS"
		echo $D_TIME $ROUTER
    	}

	DH_IF=$1
	shift 1
	RnT1=$($CAT $1 2>/dev/null | xargs echo | tr '}{' '\n' | $GREP $DH_IF | $TAIL -n 2)
	RnT2=$($CAT $1 2>/dev/null | xargs echo | tr '}{' '\n' | $GREP $DH_IF | $TAIL -n 2)
	[ "$RnT1" -o "$RnT2" ] || return
	GnT=$(get_lastest)
	C_TIME=$($DATE +%s)
	[ "${GnT% *}" -gt $C_TIME ] && GWIP=${GnT#* }
}


#-- FUNCTION: determine gw --#
dgw() {
	unset GWIP
        #-- for fixed if --#
        if echo $fixed_if | $GREP -wq $1 ; then
                GWIP=`echo -e "${fixed_gw// /\n}" | $GREP "^$1:" | \
                        $CUT -d: -f2`
                WEIGHT=`echo -e "${fixed_gw// /\n}" | $GREP "^$1:" | \
                        $CUT -d: -f3`
        #-- for ppp if --#
        elif echo $1 | $GREP -q ppp ; then
                GWIP="`$IP address show dev $1 | $AWK '/inet[^6]/{print $4}' | \
                        $CUT -d'/' -f1 | $HEAD -n 1`"
	else
	#-- try current gw --#
		GWIP=$($ROUTE -n | $GREP $1'$' | $AWK '/^0.0.0.0/{print $2}' | $HEAD -n 1)
	fi
	#-- for dhcp if --#
	[ "$GWIP" ] || dhgw $1 $DHCP_DIR/dhclient.leases $DHCP_DIR/dhclient-$i.leases
}

#-- config rule & routing --#
for i in $interfaces ; do
        unset IFIP GWIP WEIGHT
        IFIP="`$IP address show dev $i | $AWK '/inet[^6]/{print $2}' | \
                $CUT -d'/' -f1`"
        dgw $i
        [ "$tb_num" ] && tb_id=${tb_num%% *}
        [ "$IFIP" -a "$GWIP" ] || continue
	# check gw
        $IP route replace $fixed_dest via $GWIP dev $i &>/dev/null && \
        	sleep 2; $PING -c1 -w2 $fixed_dest &>/dev/null && GWOK=true
        $IP route del $fixed_dest via $GWIP dev $i &>/dev/null
	if [ "$GWOK" = true ]; then
		unset GWOK
	else
		continue
	fi
        # set rule
        $IP rule add pref $tb_id from $IFIP table $tb_id
        # set route to gw
        $IP route replace default via $GWIP dev $i table $tb_id
        DEFGW=$DEFGW" nexthop via $GWIP dev $i weight ${WEIGHT:=1}"
	SETGW=true
        tb_num=${tb_num#* }
done

#-- apply routing --#
[ "$SETGW" = true ] && {
	#-- remove old default route --#
	DEF_NU=$($IP route | $GREP -c '^default')
	for ((i=1;i<=$DEF_NU;i++)); do
		$IP route del default
	done
	#-- set new default route --#
	$DEFGW
}

#-- apply static routes --#
[ -f $STATIC ] && . $STATIC

#-- flush cache --#
$IP route flush cache

#-- show routing
$IP route list

#-- FINISH --#
exit 0

----------------------- cut here ----------------------


* 参数说明:
	-d "<interface...>"
		指定失效界面,可设定多个。


6.3 statics

* 说明:
此script 用来设定静态路由,封包将只会从单一连线送出,而不会分摊至其他连线。

* 代码:
----------------------- cut here ----------------------
#!/bin/bash
#
# script name: statics
# purpose: set static routes.
# author: netman(netman@study-area.org)
# license: GPL(http://www.gnu.org/licenses/gpl.html)
#
# date: 2004-09-23
# version: v0.02
#
# caveate:
# 1) tested on redhat 9.0 linux only
# 2) iproute program is required
#
#------------------------------------------------- -----------
# change log:
# 2004-07-08 v0.01
# * first beta release2
# 2004-09-23 v0.02
# * Improvment:
# - using flexible static_xxxx variable

WD=${0%/*}
CONFIG=$WD/configs
[ -f $CONFIG ] && . $CONFIG || {
        echo "${0#*/}: ERROR: can not load $CONFIG."
        exit 1
}

set_static () {
	while [ "$2" ]; do
		eval s_dest=\$$1
		eval s_order=\$$2
		all_gw="$($IP route show | $SED -n '/^default/,$p')"
		for if in $s_order; do
			if echo "$all_gw" | $GREP -q "$if" ; then
				sn_gw=$(echo "$all_gw" | $GREP $if | awk '{print $3}')
				sn_gw=${sn_gw%/*}
				break
 			fi
		done
		[ "$sn_gw" ] && {
		$IP route replace "$s_dest" via $sn_gw
		}
		shift 2
	done
}
set_static ${!static_*}

----------------------- cut here ----------------------

* 注意要点:
	此script 可由run_ip.sh 来呼叫,也可以独立使用。


6.4 chk_line.sh

* 说明:
此script 用来侦测连线是否正常,并定期执行run_ip.sh 及ddns.sh(选用)。
若抓出失效界面,再用-d 参数传给run_ip.sh ,该界面将被忽略。

* 代码:
----------------------- cut here ----------------------
#!/bin/bash
#
# script name: chk_ip.sh
# purpose: checking outbound link and change route and dns.
# author: netman(netman@study-area.org)
# license: GPL(http://www.gnu.org/licenses/gpl.html)
#
# date: 2004-09-23
# version: v0.04
#
# caveate:
# 1) tested on redhat 9.0 linux only
# 2) using PING for link detection
# 3) iproute program is required
# 4) scripts run_ip.sh & ddns.sh are required too
#
#------------------------------------------------- -----------
# change log:
# 1) 2004-03-23​​ v0.01
# * first beta release
# 2) 2004-03-24 v0.02
# * add script path detection
# 4) 2004-07-07 v0.03
# * IMPROVEMENT
# - Create central config file
# * Add function:
# - Add email notification
# 5) 2004-09-23 v0.04
# * IMPROVEMENT
# - Rewrite the method of getting dead interface
#
#------------------------------------------------- -----------

WD=${0%/*}
CONFIG=$WD/configs
[ -f $CONFIG ] && . $CONFIG || {
        echo "${0#*/}: ERROR: can not load $CONFIG."
        exit 1
}

# change working directory
cd $WD

#-- determine gateways --#
get_gw () {
        defaults=$($IP route show | $SED -n '/default/,$p' | $GREP via)
        if_n_gw=$(echo -en "$defaults" | $AWK '{print $5":"$3":'$fixed_dest'"}')
        if [ -n "​​$fixed_gw" ]; then
		fixed_pa​​ir=$(echo -e "${fixed_gw// /\n}" | awk -F: '{print $1":"$2":'$fixed_dest'"}')
		dyn_pair=$(echo "$if_n_gw" | $GREP -Ev "$(echo $fixed_if | $TR ' ' '|')")
        	all_pair=$(echo $fixed_pa​​ir $dyn_pair)
	else
        	all_pair=$(echo "$if_n_gw")
	fi
}

#-- configure static route --#
get_dev () {
        for i in "$@"; do
                if=${i%%:*}
                gw=$(echo $i | $CUT -d: -f2)
                dest=${i##*:}
                $IP route replace $dest via $gw dev $if && \
			sleep 1; $PING -c1 -w2 $dest &>/dev/null || echo $if
                $IP route del $dest via $gw dev $if
        done
        unset i
}

#-- run loop --#
while : ; do
        get_gw # have gw
        d_dev=$(echo $(get_dev $all_pair)) # have dead link

        [ "$d_dev" ] && { # send mail if dead link found
                echo "dead link found: $d_dev" | \
                mail -s "$(hostname): dead link" $MAIL
        }

        $SH $RUN_SCRIPT -d "$d_dev" # to run ip script

        [ -f "$DDNS_SCRIPT" -a "$DDNS" = yes ] && $SH $DDNS_SCRIPT

        unset all_pair
        sleep $INTERVAL
done

----------------------- cut here ----------------------



6.5 ddns/ddns.sh (选用项目)

* 说明:
此script 用来动态侦测连线位址,并动态更新dns 位址记录。

* 代码:
----------------------- cut here ----------------------
#!/bin/bash
# 
# script name: ddns.sh
# purpose: updating dns record
# author: netman(netman@study-area.org)
# license: GPL(http://www.gnu.org/licenses/gpl.html)
#
# date: 2004-09-25
# version: v0.04
#
# caveate:
# 1) tested on redhat 9.0 linux only.
# 2) iproute program is required.
# 3) a php webpage to show ip, and the lynx program are required.
# 4) ddns server and ddns key are required.
#
#------------------------------------------------- -----------
# change log:
# 1) 2004-03-23​​ v0.01
# * first beta release
# 2) 2004-03-23​​ v0.02
# * add EXCL_IF list for exception
# 3) 2004-03-23​​ v0.03
# * add -e option for appending EXCL_IF
# 4) 2004-09-25 v0.04
# * BugFix: detect gw interface list
#
#------------------------------------------------- -----------

# set commands
HOST=/usr/bin/host
GREP=/bin/grep
CAT=/bin/cat
IP=/sbin/ip
SED=/bin/sed
AWK=/bin/awk
LYNX=/usr/bin/lynx
TOUCH=/bin/touch
DIFF=/usr/bin/diff
NSUPDATE=/usr/bin/nsupdate
MV=/bin/mv

# set variables
if echo $0 | $GREP '^/' ; then
    w_dir=${0%/*}
else
    w_dir=$PWD/${0%/*}
fi
KEY_FILE=$w_dir/Ktestddns.+157+14615.key
UPDATE_DATA=$w_dir/nsupdate.data
UPDATE_OLD=$UPDATE_DATA.old
HOST_NAME=testddns.study-area.org
NS_SERVER=dns.study-area.org # don't use IP address
IP_SERVER=dns.study-area.org # server running ip.php program
IP_SERVER_IP=$(echo $($HOST $IP_SERVER) | tail -n 1 | $AWK '{print $NF}')
IP_URL="http://$IP_SERVER/ip.php"
CLIENT_TYPE="client" # no proxy or using ISP's proxy
#CLIENT_TYPE="proxy" # use local proxy
MASQ_IF="eth1" # which masqueraded behine NAT
#EXCL_IF="eth1" # note: also use -e options to append list

# have exception
while getopts "e:" opt; do
    case $opt in
	e) EXCL_IF=$(echo $EXCL_IF $OPTARG) ;;
    esac
done

# ensure key files
for file in $KEY_FILE ${KEY_FILE%key}private
do
    if [ ! -r $file ]; then
	echo "$(basename $0): ERROR: $file is not readable."
	exit 1
    fi
done

# ensure the server is connectable
$HOST $NS_SERVER $NS_SERVER | $GREP -q "^$NS_SERVER has address" || {
    echo "$(basename $0): ERROR: could not contact nameserver $NS_SERVER."
    exit 2
}

# prepare initial script
$CAT >| $UPDATE_DATA <<end server="" $ns_server="" update="" delete="" $host_name="" a="" end="" test="" "$?"="0" ||="" {="" echo="" "$(basename="" $0):="" error:="" could="" not="" create="" $update_data."="" exit="" 3="" }="" #="" get="" interfaces="" all_if="$($IP" route="" show="" |="" $sed="" -n="" '="" default="" ,$p'="" $awk="" via="" {print="" $5}')="" [="" "$excl_if"="" ]="" &&="" "$all_if"="" $grep="" -ev="" "${excl_if="" |}")="" masqueraded="" ip="" get_mip="" ()="" for="" m_if="" in="" $masq_if;="" do="" $all_if="" -qw="" "$m_if"="" continue="" m_gw="$($IP" |\="" '$m_if'="" $3}')="" $ip="" replace="" $ip_server_ip="" $m_gw="" sleep="" 2="" m_ip="$($LYNX" --dump="" $ip_url="" "$client_type"="" -eo="" '([0-9]{1,3}\.){3}[0-9]+')="" del="" -z="" "$m_ip"="" "${0##*="" }:="" can't="" determine="" $m_if."="" 4="" $m_if:$m_ip="" unset="" done="" current="" if="" $all_if;="" "$masq_if"="" -wq="" "$if";="" then="" new_ip="$(get_mip" -f:="" '$if'="" $2}')="" else="" address="" dev="" $if="" inet="" fi="" $new_ip="" "$new_ip"="" "update="" add="" 0="" $new_ip"="">> $UPDATE_DATA
echo "send" >> $UPDATE_DATA

# do a test then update
$TOUCH $UPDATE_OLD
if ! $DIFF $UPDATE_OLD $UPDATE_DATA; then
	$NSUPDATE -k $KEY_FILE -v $UPDATE_DATA && {
		$MV $UPDATE_DATA $UPDATE_OLD
	}
fi

#-- end of script --#

----------------------- cut here ----------------------

* 变数设定说明:
	KEY_FILE=
		用以更新dns 记录的key 。
	UPDATE_DATA=
		储存nsupdate 所需的命令稿。
	UPDATE_OLD=
		上一次nsupdate 所需的命令稿。
		script 会比对前一份命令稿内容,若相同,则不做update ,否则才做。
	HOST_NAME=
		主机名称。需与ddns 所设定的记录一致。
	NS_SERVER=
		ddns server 之主机名称,不能使用IP 位址。
	IP_SERVER=
		侦测IP 用的主机名称。
	IP_SERVER_IP=
		侦测IP 用的主机位址。
	IP_URL=
		侦测IP 用的网页URL 。
	CLIENT_TYPE=
		主机连线类型。其值为"client" 或"proxy" 二者之一。
		若主机没有使用proxy 或使用ISP 提供的proxy,请设为"client"。
		若主机使用local LAN 的proxy ,则请设为"proxy"。
	MASQ_IF=
		连线将会经过NAT 的界面,但其伪装位址需作ddns 更新。
		(注意:NAT 必须要有相应的port mapping 设定。)
	EXCL_IF=
		排除在ddns 更新之外的界面。
		通常是连线将会经过NAT 的界面,但其伪装位址不作ddns 更新。

* 参数说明:
	-e "<interface...>"
		排除在ddns 更新之外的界面,可设定多个。
		若为多个界面,其值必须至于双引号(" ")或单引号(' ')之中,如:
			-e "eth0 eth1"
		参数值将会扩充至$ESCL_IF 变数里面。

* 注意要点:
	1) 关于动态ddns 的server 及client 端设定,请另行参考:
		http://www.study-area.org/tips/ddns.htm
	2) 请定期检测及确认ddns server 能够连线及正常运作。
	3) 当前实作范例,是将ddns.sh 与相关的keys 置于ddns 子目录中。

6.6 软件包
我将本实作所用到的script 及其它相关档案包装为tarball ,其内容如下:
	multipath/
		工作目录
	multipath/run_ip.sh
		路由更新script
	multipath/chk_line.sh
		连线检测script
	multipath/ddns/
		ddns 子目录
	multipath/ddns/ddns.sh
		ddns 更新script
	multipath/ddns/Ktestddns.+157+14615.key
		ddns key
	multipath/ddns/Ktestddns.+157+14615.private
		ddns private key
	multipath/configs
		共用变数
	multipath/kernel-patch/
		kernel-patch 子目录
	multipath/kernel-patch/2.4.25-mp01
		kernel 2.4.25 config 档
	multipath/kernel-patch/patch-2.4.25-ja2.diff
		kernel 2.4.25 修补档
	multipath/statics
		静态路由script
大家可以从如下位址下载整个软件包:
	http://www.study-area.org/linux/src/multipath.tgz


6.7 执行script
以上script 均可独立执行, 其种最为主要的script 是chk_line.sh , 可用多种方式执行, 
因为是无穷回圈, 可用backupground 方式启动: 
	./chk_line.sh &
执行killall chk_line.sh 时结束.

另一种方式是以init 用respawn 方式来执行. 修改/etc/inittab, 增加: 
	ip:35:respawn:/path/to/chk_line.sh
然后执行:
	init q
要结束的话, 需修改ini​​ttab, 在句子前加# 注解, 再重跑init q 即可.



七、关于kernel patch

7.1 关于patch-2.4.25-ja2.diff 
本实作方案不一定需要进行kernel path ,经测试是可用的。
只是因为routing cache 的关系,通往同一个destination 的routing 可能在某一段时间内是不变的。
参考http://www.lartc.org/howto/lartc.rpdb.multiple-links.html 最后一段有提到: 
	Note that balancing will not be perfect, as it is route based, and routes are cached. 
	This means that routes to often-used sites will always be over the same provider.  

这对于某些需要作session 记忆的连线来说(如连接phpBB 讨论版),是不错的。
但倘若多条session 在某一特定时间内均为同一目标位址,其流量并不会分摊到其它线路上。
如果要做到将不同的session 分摊开来,而不论其目标位址是否相同,则须要下载patch 修补。

我在RedHat 9.0 上另行下载kernel 2.4.25 源始码及patch-2.4.25-ja2.diff 这支patch 。
大家可从前面6.4 章节提供的tarball 获取这次实作所用的config 及patch 。
或至官方网站下载:
	http://www.ssi.bg/~ja/patch-2.4.25-ja2.diff

7.2 关于equailze
虽然前述patch 能够将不同的session 分摊至不同连线,但这仍然是by session 的分摊,
并非做到by packet 层级的分摊,也就是所谓的"均衡负载"(equailze)。
要做到这点,似乎还要下载另一支patch:
	http://www.teatime.com.tw/~tommy/linux/equalize.patch
不过,遗憾的是,这部份我并没有测试成功。
且后来也没在kernel 2.4.25 上与另一支patch 混合使用。

关于equailze 的探讨及设定,大家可参考study-area 的讨论:
	http://phorum.study-area.org/viewtopic.php?t=10085
若有人成功测试起来,欢迎回报分享。谢谢﹗


八、展望

当前实作并没提及频宽控管。若日后有时间,再来探讨QOS 及cbq 相关的题目。
在这之前,大家不妨先参考:
http://peterkim.cgucccc.org/document/MPath.html


九、参考资料

http://phorum.study-area.org/viewtopic.php?t=10085
http://phorum.study-area.org/viewtopic.php?t=9057
http://www.teatime.com.tw/~tommy/doc/multipath.txt
http://www.study-area.org/tips/m_routing.htm
http://www.lartc.org/howto/lartc.rpdb.multiple-links.html
http://peterkim.cgucccc.org/document/MPath.html


------- 本文结束------

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值