SHELL TIPS: rsync 和 crontab 变量

本周我遭遇了一个惨痛事件,远程开发机有两块硬盘同时损坏,整个分区数据完全丢失。这直接导致我在开发机上 home 目录中所有文件人间蒸发了,真是一觉回到解放前!

值得庆幸的是这件事发生在端午假期中间,放假前我把大部分源代码都提交到了库里,辛苦劳动的损失倒不大。但是之前种种努力搭建的开发环境全丢失了,这一点让人很郁闷。为了不再遭受这样的损失,我痛下决心学习了一下 rsync,用来同步和备份数据。

不得不说 rsync 也是一个神器啊,man rsync 发现有各种灵活用法,最有用的我觉得还是通过 ssh 来备份的方法。发现了这个秘密后,我觉得用来备份自己的网站数据也不错。下面是我使用的参数:

rsync -avzuptS --delete --max-delete=100 -e "ssh" username@solrex.org:~/ ~/backup

如果懒得输入 ssh 密码,可以使用 ssh 公钥认证

一旦配置了公钥认证,这个备份命令就可以定制为 cron 任务定时执行了。这时候我发现一个有趣的问题:如果镜像了整个 home 目录的话,无法记录命令执行日志。 因为无论日志写入到 home 目录下哪个地方,都会因为远端没有这个文件被 rsync 删除掉。无奈之下只好使用笨方法,让 cron 把同步日志发邮件给自己。

$ crontab -l
SHELL=/bin/bash
HOME=/home/work
MAILTO="someone@somesite.com"
PATH=/home/work/local/bin:/usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin
00 21 * * * mirror_site

把 SHELL 配成 /bin/bash 的原因是 /bin/sh 可能会导致一些不常见的诡异问题,例如这个。同样,在 crontab 中声明 PATH 环境变量可以避免某些命令找不到的问题,特别是自己写的放在 home 目录的脚本。

Shell Tips: cppath、scppath、mybackup

分享几个觉得有用的小 shell 函数。

1. scppath

在进行一些跨机器的操作时,每次 scp 总要手动去拼那个路径,首先从 PS1 拷贝粘贴用户名和主机名,然后再 pwd 拷贝粘贴当前目录,然后再 ls 拷贝粘贴要 scp 的文件名。好烦啊,所以就写了下面这个小函数来生成 scp 的文件路径,放到 ~/.bashrc 里。

function scppath()
{
    local _IFS=$IFS
    IFS=$(echo -en "\n\b")
    local _file
    for _file in $@; do
        echo "\"$USER@$HOSTNAME:$PWD/$_file\""
    done
    IFS=$_IFS
}

2. cppath

同样可以有 cppath。

function cppath()
{
    local _IFS=$IFS
    IFS=$(echo -en "\n\b")
    local _file
    for _file in $@; do
        echo "\"$PWD/$_file\""
    done
    IFS=$_IFS
}

3. mybackup

这个函数是偷懒备份用的。当写代码写到一半,不想或者不能 check in,但又想备份一下时,就用这个命令对文件或者目录进行自动的备份。

function mybackup()
{
    local _bak_dir=~/history
    local _path=''
    mkdir -p $_bak_dir
    local _IFS=$IFS
    IFS=$(echo -en "\n\b")
    for _path in $@; do
        if [ -f $_path ]; then
            cp $_path $_bak_dir/"$_path".`date +%Y-%m-%d.%H-%M-%S`
        elif [ -d $_path ]; then
            _path=`basename $_path`
            tar -cvf $_bak_dir/$_path.`date +%Y-%m-%d.%H-%M-%S`.tar $_path
        fi 
        echo "Backuped $_path to $_bak_dir."
    done
    IFS=$_IFS
}

Poderosa 2009 特别版

自从讨厌了 Putty 黑黑的界面之后,在 Windows 下我一直使用 Poderosa 登录 ssh 主机。与 Putty 相比,Poderosa 的优点是支持标签和 Cygwin shell。 原生的 Cygwin shell 窗口太丑陋了,和 Linux 下的终端没办法比,相信经常在 Windows 下使用 Cygwin 的同志都会有同感。Poderosa 能使 Cygwin 的终端窗口获得与 Linux 终端类似的使用感受,这是我偏爱它的一个重要原因。

当然,国产的 Fterm 也支持登录 ssh 主机,使用起来也凑合,但是很多 ssh 的高级功能是不支持的。

我以前曾在这篇文章中推荐过 Poderosa,但是和很多开源软件一样,一旦遇到困难(比如主要开发人员流失),软件的升级就陷入了停滞。Poderosa 从 2006 年 11 月 22 日发布 4.10 版本之后就再也没有更新,虽然 SF Project 的 Activity 中一片对 BUG 的抱怨之声。

一直以来我对 Poderosa 最重要的不满是编码和按键问题。Poderosa 是日本人写的,所以在编码中只有ISO-8859-1、UTF-8 和日文支持,缺少对 GBK 中文编码的支持。那么在 Cygwin shell 中执行一些 Windows 原生命令比如 ipconfig 时,命令输出的中文就会是乱码;按键问题主要体现在登录到远程主机时一些按键不支持,比如 Home 键就无法正常使用。

虽然我很早之前就想自己添加进去这些特性,因为不懂 C# 语言,一直没有动手。昨天实在忍不住了,把 Poderosa 的源代码下载下来,准备学一下 C# 语言然后去修改它。

但是很不幸幸运的是,我看到 Poderosa 的 Activity 中 4 天前(09 年 1 月 2 日)增加了一篇 post,一个咱们的同胞xjzhang1979说:他改进了 Poderosa,我下载了一看,我想要的功能都有了,真开心。

xjzhang1979 将软件包上传到了一个网络文件共享网站,您可以点击这个链接下载:http://www.box.net/shared/7n7ps57jgn。为了避免该链接失效,我在我的共享网站做了一个备份,您也可以到这里去下载:http://share.solrex.org/ibuild/

PS: 后来搜索找到了作者的博客,关于此修改版介绍的原文在这

2009-03-29: 更新的 Poderosa 特别版在这里:http://share.solrex.org/ibuild/

内网穿透反向隧道代理技术

本文的主要目的是利用使 ssh 服务器(外网)通过客户端(内网)代理上网以及反向控制客户端(内网),不需要网络管理员权限,不需要 NAT。即可上网主机 A 位于内网,不可上网主机 B 位于外网,A 能直接访问 B,但 B 无法访问防火墙内的 A,这样 B 就可以使用 SSH 隧道访问 A 主机上的代理服务器上网。(估计有这种变态需求的人只存在于大学中)

B(210.77.*.*)---->(210.77.*.*)FW(192.168.0.*)|--->(192.168.0.*)A---->(192.168.0.*)FW(123.*.*.*)---->Internet
B(210.77.*.*)< ----(210.77.*.*)FW(192.168.0.*)-----(192.168.0.*)A<----(192.168.0.*)FW(123.*.*.*)<----Internet

一、代理

首先在客户端上安装 socks 代理(HTTP 代理可以使用 Squid,同理)。socks 代理软件使用 Dante。Dante 的启动以及配置需要手动调整,下面是 Dante 的配置文件 /etc/sockd.conf:

compatibility:reuseaddr
internal:0.0.0.0 port = 1080
external:eth0
logoutput:/var/log/sockd/sockd
clientmethod:none
method:none
user.privileged:root
user.notprivileged:solrex
connecttimeout:60
iotimeout:86400

## client access rules
client pass {
 from: 127.0.0.1/0 port 1-65535 to: 0.0.0.0/0
 log: connect disconnect
}

## server operation access rules
#allow bind to ports greater than 1023
pass {
  from: 0.0.0.0/0 to: 0.0.0.0/0 port gt 1023
  command: bind bindreply udpassociate udpreply
  log: connect disconnect
}

pass {
  from: 0.0.0.0/0 to: 0.0.0.0/0 port 1-65535
  protocol: tcp udp
  log: connect disconnect
}

#allow outgoing connections (tcp and udp)
pass {
  from: 127.0.0.1/0 to: 0.0.0.0/0
  command: bind bindreply connect udpassociate udpreply
  log: connect disconnect
}

#allow replies to bind, and incoming udp packets
pass {
   from: 0.0.0.0/0 to: 0.0.0.0/0
   command: bind bindreply connect udpassociate udpreply
   log: connect error
}

#log the rest
block {
   from: 0.0.0.0/0 to: 0.0.0.0/0
   log: connect error
}

下面是 Dante 的启动脚本 /etc/init.d/sockd:

#!/bin/sh
set -e

. /lib/lsb/init-functions

[ -f /usr/local/sbin/sockd ] || exit 0

[ ! -f /etc/sockd.conf ] && exit 1
SOCKD_CONF="-f /etc/sockd.conf"

SOCKD_OPTS="-D"

case "$1" in
  start)
    # Start daemons.
    log_daemon_msg "Starting Dante socks proxy server" "sockd"
    if start-stop-daemon --start --quiet --oknodo --pidfile /var/run/sockd.pid --exec /usr/local/sbin/sockd -- $SOCKD_OPTS; then
	    log_end_msg 0
	else
	    log_end_msg 1
	fi
    ;;
  stop)
    # Stop daemons.
    log_daemon_msg "Stoping Dante socks proxy server" "sockd"
	if start-stop-daemon --stop --quiet --oknodo --pidfile /var/run/sockd.pid; then
	    log_end_msg 0
	else
	    log_end_msg 1
	fi
	;;
  restart)
    log_daemon_msg "Restarting Dante socks proxy server" "sockd"
	start-stop-daemon --stop --quiet --oknodo --retry 30 --pidfile /var/run/sockd.pid
	if start-stop-daemon --start --quiet --oknodo --pidfile /var/run/sockd.pid --exec /usr/local/sbin/sockd -- $SOCKD_OPTS; then
	    log_end_msg 0
	else
	    log_end_msg 1
	fi
	;;
  *)
    log_action_msg "Usage: /etc/init.d/sockd {start|stop|restart}\n"
    exit 1
esac

exit 0

二、ssh 服务器

服务器端 ssh 服务器最好采用 Openssh,Windows 下应该使用 Cygwin 运行 sshd,FreeSSHd 实践证明崩溃比较频繁。同一台机器的 Windows 和 Linux 上运行的 ssh 服务器最好使用相同的 host key,将 /etc/ssh/ 下面的文件保持一致即可。最好配置服务器为使用公钥进行登录验证,这样能使用自动脚本进行端口映射。

三、手动建立隧道(客户端到服务器端口映射)

用下面命令建立客户端到服务器的端口映射,将客户端的 socks 代理端口 1080 映射到服务器的端口 8080。这样服务器就可以通过自己的 8080 端口反向隧道到客户端的 socks 代理 1080 端口上网。

ssh -C -f -N -g -o PreferredAuthentications=publickey -R SERVER:8080:127.0.0.1:1080 USRNAME@SERVER
# 参数含义:
# -C: 要求对数据进行压缩
# -f:要求 ssh 执行完交互后进入后台运行
# -N:不用建立一个终端
# -g:允许远程服务器连接客户端转发端口
# -R SERVER:8080:127.0.0.1:1080:将客户机(127.0.0.1)的 1080 端口绑定到服务器(SERVER)的 8080 端口。
# USRNAME@SERVER:ssh服务器的用户名和密码
# -p 3022:ssh服务器的端口

为了方便控制,最好也将客户端的 ssh 服务器端口映射到服务器端,服务器端就可以通过登录自己的 8022 端口来登录客户端的 ssh 服务器:

ssh -C -f -N -g -o PreferredAuthentications=publickey -R SERVER:8022:127.0.0.1:22 USRNAME@SERVER

这样两台被防火墙隔开的主机就能实现双向控制。

四、自动建立隧道

手动建立隧道的缺陷是服务器端必须长期开机,并且连接只能用户在客户端手动发起。可以考虑间接的办法,比如使用两台主机均可访问的服务器作为跳板,或者客户端自动登录聊天软件,利用聊天软件接受指令。下面给出一种使用共享服务器的方法:

#!/bin/bash
# {start|stop|restart:username:ipaddress}

COMMAND="stop"
SERVER="0.0.0.0"
USRNAME=""
PORT="22"

INFOURL="http://someserver.com/somepage"

SSHOPTS="-C -f -N -g -o PreferredAuthentications=publickey -o StrictHostKeyChecking=no"

get_server_info()
{
  #info=`wget -nv -O - $INFOURL 2> /dev/null | iconv -f gbk -t utf8 |
        grep -o -e "{.*}" | tr -d '{}'`
  info=`wget -nv -O - $INFOURL 2> /dev/null | iconv -f gbk -t utf8 |
        sed -n "/{*}/s/.*{\(.*\)}.*/\1/p"`
  COMMAND=${info%%:*}
  SERVER=${info#*:}
  USRNAME=${SERVER%:*}
  SERVER=${SERVER#*:}
}

tunneling_status()
{
  tun_ps=`ps aux | grep "ssh -C" | wc -l`
  if [ $tun_ps -gt 4 ]; then
    echo -n "running"
  else
    echo -n "died"
  fi
}

start_tunneling()
{
  ssh $SSHOPTS -R 3128:127.0.0.1:3128 ${USRNAME}@${SERVER} -p $PORT
  ssh $SSHOPTS -R 8022:127.0.0.1:22 ${USRNAME}@${SERVER} -p $PORT
}

failsafe_tunneling()
{
  ssh $SSHOPTS -R 8022:127.0.0.1:22 ${USRNAME}@${SERVER} -p $PORT
}

stop_tunneling()
{
  killall -e ssh
}

echo -n "[`date +%F\ %R`] "
get_server_info

case "$COMMAND" in
  start)
    if [ $(tunneling_status) = "running" ]; then
      echo "Starting ssh tunneling.(started, do nothing)"
    else
      echo "Starting ssh tunneling."
      start_tunneling
    fi
    ;;
  restart)
    if [ $(tunneling_status) = "running" ]; then
      echo "Restarting ssh tunneling."
      stop_tunneling
      start_tunneling
    else
      echo "Restarting ssh tunneling."
      start_tunneling
    fi
    ;;
  stop)
    if [ $(tunneling_status) = "running" ]; then
      echo "Stoping ssh tunneling."
      stop_tunneling
    else
      echo "Stoping ssh tunneling.(stoped, do nothing)"
    fi
    ;;
  failsafe)
    echo "Starging ssh tunneling.(failsafe mode)"
    failsafe_tunneling
    ;;
  sleep)
    echo "Sleeping."
    exit 0
    ;;
  *)
    echo "Unrecogenized server command ($COMMAND)."
    exit 1
    ;;
esac

exit 0

将上面脚本加入 crontab 每十分钟运行一次,就能实现在服务器端对客户端进行有限的控制。

那些 ssh 教我的事

我以前以为 ssh 只能登录主机执行命令,ssh 教我它还会 scp;

我以为会 scp 很厉害了,ssh 教我它还会 sftp;

我以为会 sftp 了不得了,ssh 教我它还会 sshfs;

我以为会 sshfs 已经相当牛了,ssh 教我它还能把服务器端口绑定到本机端口(ssh -L);

我以为把服务器端口绑定到本机端口就已经上天了,ssh 教我它还能把本机端口绑定到服务器端口(ssh -R)...

然后,我怨念已久的共享上网终于实现了!

PS: 用软件还得选老牌开源软件,openssh 那么多年没更新了,在 Windows 下用 cygwin 运行都比 freesshd 稳定,不得不赞一个。Win 下的一些软件,窗口搞得花里胡哨的,功能和稳定性却不知道放到第几位了,跟人一样,长得好看的未必靠谱。