2012年5月25日星期五

IE代理设置会导致Python的urlopen变慢

import sys
import time
try:
    from urllib2 import urlopen
except:
    from urllib.request import urlopen

url = 'http://www.baidu.com'

start = time.time()
res = urlopen(url).read()
elapsed = time.time() - start
print('Elapsed time: {0}'.format(elapsed))

如果你的IE中有设置代理服务器,如上代码跑下来很可能要接近5秒,但取消代理设置之后,1秒都不需要。

pdb跟了半天,过程就不写了,直接说结论吧,问题出在urllib.py(Python 2.7.1,下同)的1534行,proxy_bypass函数:

    def proxy_bypass(host):
        """Return a dictionary of scheme -> proxy server URL mappings.

        Returns settings gathered from the environment, if specified,
        or the registry.

        """
        if getproxies_environment():
            return proxy_bypass_environment(host)
        else:
            return proxy_bypass_registry(host)

getproxies_environment函数用来获取以“_PROXY”结尾的环境变量,如果不存在则返回空的dict。也就是说,有类似的环境变量存在则执行proxy_bypass_environment函数,没有则执行proxy_bypass_registry函数。

而导致urlopen如此之慢的罪魁祸首,就在urllib.py第1511行,proxy_bypass_registry函数中的这一句:

fqdn = socket.getfqdn(rawHost)

执行getfqdn函数,会向rawHost发送三次NetBIOS Name Query请求。也就是这三次压根不会收到返回数据的请求,导致了urlopen会慢成这样。而proxy_bypass的功能,也不过就是确定某个host要不要走代理而已。

Python 2.7.1和3.2均有这个问题,我想保留这个设计总是有一定道理的吧。不过总这么delay也不是事,所以要在IE里设置代理又要用Python写的东西,只有多设置几个环境变量了:

SET HTTP_PROXY="127.0.0.1:8118"
SET HTTPS_PROXY="127.0.0.1:8118"
SET NO_PROXY="localhost,127.0.0.1"

2012年5月13日星期日

OpenWRT DNS

就我所知,目前可以应对DNS劫持的方法有以下这么些:

  1. 使用加密的通道进行DNS查询。
  2. 使用TCP协议发送DNS请求。
  3. 使用监听于非标准端口的DNS服务器。
  4. 设法挡掉伪造的DNS应答。

方案一的实现方式有VPN、Tor等等,前者需要额外的开销,后者速度和稳定性不佳,暂不考虑。

使用TCP协议发送DNS请求

由于GFW只污染了使用UDP协议发往服务器53端口的请求,所以改用TCP协议发送请求就可以规避污染。只是具体到OpenWRT,其默认采用的dnsmasq无法强制使用TCP协议向上游服务器转发请求,所以非要用这个法子,就得再装一个unbound。而unbound的资源占用对路由器来说颇为不低,所以这个方案对OpenWRT来说实用性并不强。

opkg install unbound后,修改/etc/unbound/unbound.conf:

server:
  port: 5353
  do-ip4: yes
  do-ip6: no
  do-udp: yes
  do-tcp: yes
  tcp-upstream: yes
forward-zone:
  name: "."
  forward-addr: 8.8.8.8
  forward-addr: 8.8.4.4

然后修改/etc/config/dhcp:

config 'dnsmasq'
        #option 'resolvfile' '/tmp/resolv.conf.auto'
        option 'noresolv' '1'
        list 'server' '127.0.0.1#5353'

注释掉resolvfile并打开noresolv选项,是为了让dnsmasq不使用resolvfile中的DNS服务器进行查询,下同。

使用监听于非标准端口的DNS服务器

如上所说,GFW只污染了使用UDP协议发往服务器53端口的请求,所以若是某个服务器由非标准端口提供DNS服务,同样可以规避DNS污染。

使用非标准端口的DNS服务器不多,Google DNS就不行。有个德国隐私基金会倒是提供了一组服务器,不过速度也太慢了点。于是没得选了,只剩下OpenDNS。

修改/etc/config/dhcp:

config 'dnsmasq'
        #option 'resolvfile' '/tmp/resolv.conf.auto'
        option 'noresolv' '1'
        list 'server' '208.67.222.222#5353'
        list 'server' '208.67.220.220#5353'

针对OpenDNS查询不存在的域名显示广告的问题,可以用dnsmasq的bogus-nxdomain来解决,将下面这一句加入dnsmasq的配置文件即可。(如果遇到国内流氓运营商的劫持问题,也可以用这个法子来试试。)

bogus-nxdomain=67.215.65.132

设法挡掉伪造的DNS应答

用tcpdump或Wireshark抓包可以看到,GFW会在正确的DNS应答之前加塞几条错误应答。如果我们可以分辨哪些是GFW伪造的应答并将其忽略,同样可以达到规避DNS污染的目的。

GFW伪造的应答分两种,一种返回一个错误的IP,另外一种不包含任何查询结果。针对后一种情况,AntiDNSPoisoning提供了如下规则:

iptables -I INPUT -p udp --sport 53 -m u32 --u32 "4 & 0x1FFF = 0 && 0 >> 22 & 0x3C @ 8 & 0x8000 = 0x8000 && 0 >> 22 & 0x3C @ 14 = 0" -j DROP
iptables -I FORWARD -p udp --sport 53 -m u32 --u32 "4 & 0x1FFF = 0 && 0 >> 22 & 0x3C @ 8 & 0x8000 = 0x8000 && 0 >> 22 & 0x3C @ 14 = 0" -j DROP

由于需要安装iptables-mod-u32和kmod-ipt-u32,我的OpenWRT需要重新编译,所以暂时没法实际测试。我依照原文的说法,写了下面两条规则丢掉Answer、Authority和Additional均为0的应答:

iptables -I INPUT -p udp --sport 53 -m string --algo bm --hex-string "|81 80 00 01 00 00 00 00 00 00|" --from 30 --to 40 -j DROP
iptables -I FORWARD -p udp --sport 53 -m string --algo bm --hex-string "|81 80 00 01 00 00 00 00 00 00|" --from 30 --to 40 -j DROP

至于返回错误IP的应答则有点复杂。AntiDNSPoisoning的思路是通过向伪DNS服务器查询被污染域名,获得错误IP的列表,然后再将返回这些IP的应答都丢掉。这个思路看起来可行,但问题在于如何获得完整的错误IP列表。

以下是我这里找到的错误IP:

  • 159.106.121.75
  • 37.61.54.158
  • 59.24.3.173
  • 203.98.7.65
  • 243.185.187.39
  • 78.16.49.15
  • 46.82.174.68
  • 159.24.3.173
  • 93.46.8.89
  • 243.185.187.30
  • 8.7.198.45

假如说这个列表完备,且在相对较长的一段时间内都没有变动的话,则AntiDNSPoisoning提出的方案可行。不过这两个前提条件是否成立,那就需要比较长的时间来验证了。另外,iptables的string模块是否会带来比较严重的延迟也是一个问题。毕竟按照AntiDNSPoisoning方案的思路,每个DNS查询响应都要比对十几条规则,或许为dnsmasq打个补丁来做这件事要更好一点。

除了返回的IP外,TTL值也可以用来分辨GFW伪造的DNS查询响应。比如从本机ping 8.8.8.8,得到TTL值51,则可以构造出以下规则:

iptables -I INPUT -p udp -s 8.8.8.8 --sport 53 -m ttl --ttl-lt 52 -j DROP
iptables -I INPUT -p udp -s 8.8.8.8 --sport 53 -m ttl --ttl-gt 52 -j DROP
iptables -I FORWARD -p udp -s 8.8.8.8 --sport 53 -m ttl --ttl-lt 51 -j DROP
iptables -I FORWARD -p udp -s 8.8.8.8 --sport 53 -m ttl --ttl-gt 51 -j DROP

本机的TTL为51,则路由器上少跳一次所以是52。在网络环境一定的情况下,TTL一般不会变化,而GFW伪造的应答与正确应答的TTL刚好一样的可能性也很低,所以这个法子也有一定的实用价值。

小结

从不需要安装更多的程序和规则数量两方面考虑,目前我用的是5353端口的OpenDNS。

2012年5月12日星期六

OpenWRT Tor

不知道为什么官方源里没有tor,我在网上倒是找到了一个,不过这个包是为了那些境外的爱心人士准备的,苦逼天朝民众并不适用。所以我从这个包里提取出了tor的可执行文件,自己弄了一个出来:

https://www.boxcn.net/s/f072873ad52ec5eac450

/opt/bin/tor-ctrl是我用lua写的一个tor控制脚本,运行需要opkg install luasocket,最主要的用途是通过SSH代理抓取网桥。/etc/init.d/tor start之后手工执行一次就可以,如果想的话,也可以加到cron计划任务里。

编辑/etc/config/cron,加入这么一段:

config 'task'
        option 'task_name' 'Check Tor Bridges'
        option 'task_Everyday' '1'
        option 'task_time' 'everyh_1'
        option 'task_task' '/opt/bin/tor-ctrl'
        option 'enabled' '1'

我路由器里这个Dreambox的/etc/init.d/cron脚本有bug,70行左右改成如下这样:

2)
local task_hour=`echo $task_time |cut  -d "_" -f2`

if [ $task_Everyday == "1" ] ; then
echo "0 */$task_hour * * * $task_task 2>/dev/null #${task_name} " >> /etc/crontabs/root
else
echo "0 */$task_hour * * $task_week $task_task 2>/dev/null #${task_name} " >> /etc/crontabs/root
fi

;;

在本地运行还不觉得,可对于路由器来说,Tor的资源占用还是有点大。再加上Tor还是太慢太不稳定了,留着应急吧,日常使用还是算了。

2012年5月7日星期一

OpenWRT AutoSSH

我没有VPN,并且OpenWRT默认的ssh客户端dropbear不能用来创建socks代理,并且我从SSHChina买来的帐户不支持证书登陆,所以得改个OpenSSH并且重新编译。

OpenWRT的SDK编译起来并不麻烦,只是有点耗时间。为OpenSSH写补丁也不难,将下面的内容存为999-env-pwd.patch,放到feeds/packages/net/openssh/patches下编译即可。

--- a/sshconnect2.c
+++ b/sshconnect2.c
@@ -866,6 +866,7 @@
  static int attempt = 0;
  char prompt[150];
  char *password;
+ char *env_pwd = getenv("OPENSSH_PASSWORD");
  const char *host = options.host_key_alias ?  options.host_key_alias :
      authctxt->host;
 
@@ -875,17 +876,23 @@
  if (attempt != 1)
   error("Permission denied, please try again.");
 
- snprintf(prompt, sizeof(prompt), "%.30s@%.128s's password: ",
-     authctxt->server_user, host);
- password = read_passphrase(prompt, 0);
+ if (env_pwd == NULL) {
+  snprintf(prompt, sizeof(prompt), "%.30s@%.128s's password: ",
+   authctxt->server_user, host);
+  password = read_passphrase(prompt, 0);
+ }
  packet_start(SSH2_MSG_USERAUTH_REQUEST);
  packet_put_cstring(authctxt->server_user);
  packet_put_cstring(authctxt->service);
  packet_put_cstring(authctxt->method->name);
  packet_put_char(0);
- packet_put_cstring(password);
- memset(password, 0, strlen(password));
- xfree(password);
+ if (env_pwd == NULL) {
+  packet_put_cstring(password);
+  memset(password, 0, strlen(password));
+  xfree(password);
+ } else {
+  packet_put_cstring(env_pwd);
+ }
  packet_add_padding(64);
  packet_send();

这种东西因为账户安全的原因,最好自己编译。不过我还是放了一个上来,放心的话,就用这个吧:https://www.boxcn.net/s/33ceb35647f5d510f1b2

(因为ps能看到所有进程的命令行,所以我改的方式是从OPENSSH_PASSWORD环境变量中读取登陆密码,用起来是这个样子:OPENSSH_PASSWORD=pwd ssh -CfNg -D 192.168.1.1:7070 user@host)

将下载或编译好的ssh放到/opt/bin/ssh下,然后创建一个ssh的配置文件/etc/ssh/ssh_config:

Host *.sshchina.com
  StrictHostKeyChecking no

Host可以指定IP地址,但似乎不能一行写好几个。StrictHostKeyChecking no的意思是,自动接受指定服务器的证书而不询问用户。

接下来,opkg install autossh安装好autossh。打开/etc/config/autossh,改成类似下面这样:

config autossh
        option ssh      '-CfNg -D 0.0.0.0:7070 root@host'
        option password 'pwd'
        option gatetime '0'
        option monitorport      '20000'
        option poll     '600'

再打开/etc/init.d/autossh,将start_instance段改成:

start_instance() {
        local section="$1"

        config_get ssh "$section" 'ssh'
        config_get gatetime "$section" 'gatetime'
        config_get monitorport "$section" 'monitorport'
        config_get poll "$section" 'poll'
        config_get password "$section" 'password'

        export AUTOSSH_PATH="/opt/bin/ssh"
        export OPENSSH_PASSWORD="$password"
        AUTOSSH_GATETIME="${gatetime:-30}" \
        AUTOSSH_POLL="${poll:-600}" \
        service_start /usr/sbin/autossh -M ${monitorport:-20000} -f ${ssh}
}

然后启动并启用autossh:

/etc/init.d/autossh start
/etc/init.d/autossh enable

HTTP代理方面,Polipo没法做代理调度,所以还是opkg install privoxy安装Privoxy,这样一旦搞好,家里的所有机器都只需要改代理服务器地址就行了。唯一的麻烦是,Privoxy的配置是纯文本的,改起来不是那么容易,不过这个问题以后再想办法解决吧。

/etc/privoxy/config中需要留意的就是下面三行,默认permit-access不包括localhost,连本地访问都不放过…

listen-address  0.0.0.0:8118
permit-access  192.168.1.0/24
permit-access  127.0.0.1

Update 20130928

适用于OpenWrt 12.09正式版的ssh:https://app.boxcn.net/s/hlui5boqrkpkgijfc66h