这是一种基于域名的 VPN 智能翻越方案。不同于 chnroutes 这类通过维护一个 IP 地址列表来区分国内外网站的方案,基于域名的方式不受 IP 地址变动的影响。仅需维护一个相对很少有变化的域名列表即可。
dnsmasq 在 2.66 版之后加入了对 ipset 的支持,可将指定域名的 IP 解析后自动加入某一 ipset 中。 再配置路由规制,使该 ipset 中的 IP 走 VPN 即可。
感谢 @wzyboy 提供了此方案的思路。
opkg update
opkg install ip ipset kmod-ipt-ipset dnsmasq-full
OpenWrt 默认的dnsmasq
包并不包含 ipset 支持,需安装dnsmasq-full
(感谢 @leavic 的提醒)。亦可修改 makefile 自行编译,以节省空间。
列举一下涉及的各部分配置文件,供参考。
各类 VPN 均可,请参考 VPN overview。注意请勿将 VPN 设为默认路由。
添加outwall
路由表。 走 VPN 的流量都将使用此表。
echo "200 outwall" >> /etc/iproute2/rt_tables
使系统启动时创建一个名为outwall
的 ipset。
在 /etc/rc.local 中添加:
# vi /etc/rc.local
ipset create outwall hash:ip
名字可以随便取,与 dnsmasq.conf 和 firewall.user 中的保持一致即可。
将匹配 ipset outwall
的包全部标上 mark 8。
在 /etc/firewall.user 中添加:
# vi /etc/firewall.user
iptables -t mangle -A fwmark -m set --match-set outwall dst -j MARK --set-mark 8
打上 mark 以后,就可以指定它们所使用的路由表了。
mark 值可随便选,保持一致即可。若同时装有 qos-scripts,mark 可选一个较大的值,以防与其发生冲突。
确保以下脚本在每次 VPN 连接建立后执行。
不同 VPN 的配置方法可能不同。 一个比较通用的方法是使用 Hotplug。
(我这次用的是 vpnc,所以比较偷懒地将脚本放在了/etc/vpnc/post-connect.d/
下)
#!/bin/sh
ip route add 8.8.8.8 dev $TUNDEV
ip route add default dev $TUNDEV table outwall
ip rule add fwmark 8 table outwall
注意,须将$TUNDEV
替换为 VPN 设备名,比如ppp0
。
这段脚本有三个作用:
见上节说明,须在 VPN 断线时 自动执行的脚本:
#!/bin/sh
ip rule del table outwall
请参考 dnsmasq man(8)。
修改 /etc/dnsmasq.conf,在其中加入需要翻越的域名。 格式如下:
server=/域名/8.8.8.8 ipset=/域名/outwall
server
将指定域名使用 8.8.8.8 查询(8.8.8.8 已配置为走 VPN,防止 DNS 污染);ipset
将指定域名的所有 IP 加入 ipset outwall
中;ipset=/twitter.com/t.co/outwall
;google.com
将同时匹配 google.com 和 plus.google.com。com
将匹配所有 .com 结尾域名。一个可供参考的配置如下, 该配置:
server=/google.com/8.8.8.8 server=/googleusercontent.com/8.8.8.8 server=/gstatic.com/8.8.8.8 server=/googlehosted.com/8.8.8.8 server=/golang.org/8.8.8.8 server=/googleapis.com/8.8.8.8 ipset=/google.com/outwall ipset=/googleusercontent.com/outwall ipset=/gstatic.com/outwall ipset=/googlehosted.com/outwall ipset=/golang.org/outwall ipset=/googleapis.com/outwall server=/twitter.com/8.8.8.8 server=/twimg.com/8.8.8.8 server=/t.co/8.8.8.8 ipset=/twitter.com/outwall ipset=/twimg.com/outwall ipset=/t.co/outwall server=/facebook.com/8.8.8.8 ipset=/facebook.com/outwall server=/youtube.com/8.8.8.8 server=/ytimg.com/8.8.8.8 server=/ggpht.com/8.8.8.8 server=/youtu.be/8.8.8.8 server=/googlevideo.com/8.8.8.8 server=/youtube-nocookie.com/8.8.8.8 ipset=/youtube.com/outwall ipset=/ytimg.com/outwall ipset=/ggpht.com/outwall ipset=/youtu.be/outwall ipset=/googlevideo.com/outwall ipset=/youtube-nocookie.com/outwall server=/githubusercontent.com/8.8.8.8 server=/github.global.ssl.fastly.net/8.8.8.8 server=/githubapp.com/8.8.8.8 ipset=/githubusercontent.com/outwall ipset=/github.global.ssl.fastly.net/outwall ipset=/githubapp.com/outwall
(更新于 2014-11-23)
补充说明一下,此方案亦可与 shadowsocks-libev 配合使用,配置起来会比 VPN 简单很多。自己用了几个月,体验良好。这里简单介绍一下配置方法。
1、在 shdowsocks.org 可以下载到已编译的 ipk 包,在 OpenWrt 上安装即可。
2、配置好 Shadowsocks,并运行,注意服务器那边需要支持 UDP 转发(请使用较新版并配好防火墙)。OpenWrt 中使用ss-redir
代理 TCP 连接,使用ss-tunnel
代理 DNS 查询。具体参数请参考范例:
ss-redir -c /etc/shadowsocks.json -l 1080 ss-tunnel -c /etc/shadowsocks.json -l 5533 -L 8.8.8.8:53 -u
3、在/etc/firewall.user
中加入(参考):
iptables -t nat -N shadowsocks iptables -t nat -A shadowsocks -m set ! --match-set outwall dst -j RETURN iptables -t nat -A shadowsocks -p tcp --syn -m connlimit --connlimit-above 32 -j RETURN iptables -t nat -A shadowsocks -p tcp -j REDIRECT --to-ports 1080 iptables -t nat -A PREROUTING -s 192.168.0.0/16 -j shadowsocks
这将使匹配outwall
(这个 IPSet)的 TCP 连接指向ss-redir。
后者将为这些连接建立隧道并通过 Shadowsocks 协议加密发送。
4、/etc/dnsmasq.conf
中不再使用8.8.8.8
,而是使用127.0.0.1#5533
,范例如下:
server=/google.com/127.0.0.1#5533 ipset=/google.com/outwall
这将使 dnsmasq 将 DNS 查询请求发送至ss-tunnel
,后者在将其通过 SS 转送至 Google Public DNS。
由于不再涉及路由,不需要安装配置iproute2
,不再需要使用-j MARK
打标记。当然,也不用在配置 VPN 及相关脚本。
优点:方便配置,不管是路由器还是服务器。
缺点:只代理 TCP 连接,UDP、ICMP (ping) 等无法使用。
于是做了不少尝试,比如宿舍内通过 VPN 连接自家路由器,然后将数据包通过自家路由器转发到宿舍路由器上,但这样做怎加了额外的延时,且受家里带宽限制。再比如通过 UDP 打洞 来实现双方直连..
但是如此一来,要用 UDP 来传送 TCP 内容,就需要自己实现 TCP 的各种功能,还要追踪连接,太过复杂大大超出自己能力水平(太弱了。所以就想着用各种办法偷懒。
上学期做的一个尝试是,用 tun 虚拟网卡,直接转发IP包。如此一来,TCP 全由系统实现,避开了这些复杂的事… (参见《OpenWrt 上使用 Python 操作 TAP/TUN》)最后止步于放假回家,和 Windows 下修改路由表的一些问题(很想具体喷一下但是这就扯远太了)。
前段时间忽然想到,OpenVPN 可使用 UDP 建立连接。如此一来,只要自己先(用少量代码)完成 UDP 打洞,而后的操作就可以全权交由(相当完善的) OpenVPN 处理,工作量、复杂度急剧下降。
先简单重复一下 UDP 打洞的具体过程。
实际使用时可能会受到一些限制,宿舍网络不存在此问题不再累述。
首先,在宿舍内架 OpenVPN。我在 Cubieboard 上的 Ubuntu 架的,就按通常设置就行。
UDP 打洞要求在客户端连接前,服务器发送一个 UDP 包到客户端,且必须从 OpenVPN 监听端口上发出。为了在发送时不中断 openvpnd,我用了 pylibnet 跳过系统 socket API 直接发送该包。代码如下:
#!/usr/bin/env python | |
#encoding: utf-8 | |
import libnet | |
from libnet.constants import RAW4, RESOLVE, IPV4_H, UDP_H, IPPROTO_UDP | |
IFACE = 'wlan2' # Sending via the interface. | |
def sendto(sport, address): | |
l = libnet.context(RAW4, IFACE) | |
dest_ip = l.name2addr4(address[0], RESOLVE) | |
l.build_udp(sp=sport, dp=address[1], | |
payload='\x00hehe' | |
l.autobuild_ipv4(len=(IPV4_H + UDP_H), | |
prot=IPPROTO_UDP, dst=dest_ip) | |
l.write() | |
if __name__ == '__main__': | |
sendto(6000, ('vpn.sorz.org', 6001)) |
这段代码(ovpn-snatd.py)用于告知 VPS,OpenVPN 服务器的地址端口。同时接收由 VPS 转发的客户端地址端口信息:
#!/usr/bin/env python | |
import socket | |
import sendudp | |
SNAT_BIND_PORT = 6001 | |
SNAT_SERVER = ('sorz.org', 6002) | |
OPENVPN_BIND_PORT = 1194 | |
def main(): | |
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
sock.bind(('', SNAT_BIND_PORT)) | |
sock.settimeout(20) | |
while True: | |
sock.sendto('\x00', SNAT_SERVER) | |
try: | |
sock.recv(1024) # Ignore ping response | |
data = sock.recv(1024) # Receiving users' connection request. | |
except socket.timeout: | |
continue | |
if data[0] != '\x03': | |
continue | |
print('a new connection from ' + data[1:]) | |
client = data[1:].split(':') | |
sendudp.sendto(OPENVPN_BIND_PORT, (client[0], int(client[1]))) | |
if __name__ == '__main__': | |
main() |
然后,让 VPS 帮助传递端口双方外网端口地址,总共四段代码:
#!/usr/bin/python | |
import socket | |
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
s.bind(('0.0.0.0', 6000)) | |
while True: | |
data, addr = s.recvfrom(1024) | |
s.sendto(str(addr[1]), addr) |
# (...) | |
SNAT_SERVER_PORT = 6002 | |
@app.route('/ovpn/connect') | |
def movpn_openvpn(): | |
server = get_memcache().get('movpn.openvpn.server') | |
if not server: | |
return 'Server is not running.', 404 | |
addr = request.remote_addr | |
if addr.startswith('::ffff:'): | |
addr = addr[7:] | |
port = request.args.get('port', 1194) | |
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
s.sendto('\x02%s:%s' % (addr, port), ('localhost', SNAT_SERVER_PORT)) | |
return server | |
# (...) |
ovpn-snatd.py
,同时返回给用户 OpenVPN 服务器的外网地址端口(为了方便,通过 memcache 读取)。
#!/usr/bin/env python | |
import memcache | |
SNAT_SERVER_PORT = 6002 # local listening | |
OPENVPN_BIND_PORT = 1194 | |
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
s.bind(('0.0.0.0', SNAT_SERVER_PORT)) | |
mc = memcache.Client(['127.0.0.1:11211']) | |
server = mc.get('movpn.openvpn.server') | |
if server: | |
server = (server.split(':')[0], OPENVPN_BIND_PORT) | |
while True: | |
data, addr = s.recvfrom(1024) | |
if data[0] == '\x00': # From openvpn server | |
if addr != server: | |
server = addr | |
mc.set('movpn.openvpn.server', '%s:%s' % (server[0], OPENVPN_BIND_PORT)) | |
s.sendto('\x01', addr) | |
elif data[0] == '\x02': # From local web server (user's conn request) | |
if addr[0] != '127.0.0.1': | |
print('\x02 != localhost') | |
continue | |
print('new connect from ' + data[1:]) | |
s.sendto('\x03%s' % data[1:], server) | |
else: | |
print('unknown') |
用户启动 OpenVPN 前,先随机生成一个 UDP 端口,通过getported.py
取得该端口对应的外网端口。然后通过 HTTP 将端口号传递给view.py
,同时取得服务器地址。最后释放该 UDP 端口,让给 OpenVPN ,由它建立连接。代码如下:
#!/usr/bin/env python | |
import random | |
import logging | |
import subprocess | |
import time | |
import socket | |
import requests | |
NAT_REQUEST_URL = 'https://vpn.sorz.org/ovpn/connect?port=%s' | |
SNAT_SERVER = ('vpn.sorz.org', 6000) | |
def get_default_param(): | |
return ['bin\openvpn.exe', | |
'--client', | |
'--bind', | |
'--local', '0.0.0.0', | |
'--proto', 'udp', | |
'--dev', 'tun', | |
'--resolv-retry', 'infinite', | |
'--persist-key', | |
'--persist-tun', | |
'--ca', 'ca.crt', | |
'--cert', 'testclient.crt', | |
'--key', 'testclient.key', | |
'--ns-cert-type', 'server', | |
'--keepalive', '20', '60', | |
'--comp-lzo', | |
'--verb', '3', | |
'--mute', '20', | |
'--script-security', '2', 'system' | |
] | |
def main(): | |
logging.basicConfig(level=logging.DEBUG, | |
format='%(asctime)s %(levelname)-8s %(message)s', | |
datefmt='%Y-%m-%d %H:%M:%S', filemode='a+') | |
logging.info('Version 0.2a1') | |
port = random.randint(8192, 65535) | |
logging.info('Use random port %s.' % port) | |
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
sock.bind(('0.0.0.0', port)) | |
sock.settimeout(3) | |
nat_port = None | |
for i in range(5): | |
try: | |
sock.sendto('orz', SNAT_SERVER) | |
nat_port = int(str(sock.recv(256))) | |
except socket.timeout: | |
logging.warn('Timeout, retry getting NAT port.') | |
continue | |
except ValueError: | |
logging.warn('Illegal value, retry getting NAT port.') | |
continue | |
sock.shutdown(socket.SHUT_RDWR) | |
sock.close() | |
if nat_port is None: | |
logging.error("Can't get NAT port. Using local bind port.") | |
nat_port = port | |
logging.info('NAT Port is %s.' % nat_port) | |
r = requests.get(NAT_REQUEST_URL % nat_port) | |
if r.status_code == 404: | |
logging.error('Server is offline.') | |
return | |
server = r.text.strip() | |
logging.info('Server address is %s.' % server) | |
logging.info('Waiting 2 seconds...') | |
time.sleep(2) | |
openvpn = get_default_param() | |
openvpn.extend(['--remote'] + server.split(':')) | |
openvpn.extend(('--lport', str(port))) | |
logging.info('Calling openvpn') | |
subprocess.call(openvpn) | |
if __name__ == '__main__': | |
main() |
bin\
。连接前只要安装 Tun/tap 驱动就行了。Linux 下把bin\openvpn.exe
改成openvpn
即可。在 Windows 7 和 Ubuntu 下测试过没问题。
另外,因为只是一次尝试并未正式投入实用,所以通信时并未做认证和加密。为避免安全隐患,代码发布前经过少量修改,但修改后未经实际测试。如有疏漏请留言,谢谢。
当时做得就挺乱的,现在讲得更乱,还请求轻喷。
]]>Squid 通过 ACL 为每个访问分类,为每个请求分类,控制行为。与目的网站相关的有:dstdomain
(目标域名)、dstdom_regex
(目标域名,正则表达式) 和dst_as
(目标 AS 号)等,具体用法见 官方文档。
GFWList 使用的是 Adblock 一样的格式,给它全转成正则表达式,然后使用dstdom_regex
匹配就好了。写了个转换脚本(gfwlist2regex.py),运行后自动下载转换生成黑白名单,方便日后更新。
#!/usr/bin/env python | |
#encoding: utf-8 | |
import urllib2 | |
from base64 import b64decode | |
LIST_URL = 'https://autoproxy-gfwlist.googlecode.com/svn/trunk/gfwlist.txt' | |
BLACK_FILE = 'gfw.url_regex.lst' | |
WHITE_FILE = 'cn.url_regex.lst' | |
def convert_line(line): | |
if line[0] == '/' and line[-1] == '/': | |
return line[1:-1] | |
line = line.replace('*', '.+') | |
line = line.replace('(', r'\(').replace(')', r'\)') | |
if line.startswith('||'): | |
return '^https?:\/\/%s.*' % line[2:] | |
elif line.startswith('|'): | |
return '^%s.*' % line[1:] | |
elif line[-1] == '|': | |
return '.*%s$' % line | |
else: | |
return '.*%s.*' % line | |
def convert(gfwlist): | |
black = open(BLACK_FILE, 'w') | |
white = open(WHITE_FILE, 'w') | |
for l in gfwlist.split('\n'): | |
l = l[:-1] | |
if not l or l[0] == '!' or l[0] == '[': | |
continue | |
if l.startswith('@@'): | |
white.write(convert_line(l[2:]) + '\n') | |
else: | |
black.write(convert_line(l) + '\n') | |
def main(): | |
src = urllib2.urlopen(LIST_URL).read() | |
src = b64decode(src) | |
convert(src) | |
if __name__ == '__main__': | |
main() |
修改 Squid 设置,例如这样:
# ...
acl cn dstdom_regex 'cn.url_regex.lst'
acl gfwed dstdom_regex 'gfw.url_regex.lst'
prefer_direct on
# 默认直连
never_direct allow gfwed
never_direct deny cn
# 白名单直连失败将尝试使用代理;如使用
# always_direct allow cn 将禁止白名单使用代理。
cache_peer localhost parent 1234 0 name=s1 weight=5
cache_peer localhost parent 4321 0 name=s2 weight=10
# ...
黑名单里的必须走上级代理(cache_peer);其余先尝试直接访问,如果被重置,将会转而尝试上级代理。挺智能的。
]]>SSH -D
替代品。
SOCKS5 代理自己用着是没问题的,但普及以及携带使用的时候,HTTP 代理会更方便些(无 DNS 污染问题)…
代码在这,基于 0.9.2。
先放 pastebin 上了.. git 用不清楚就先不 fork 了.. – –
(虽然改得比较丑,但使用起来没问题啦
RT @luosheng: 靠,GFω 你赢了…… RT @Ubuntu_Tips: 各位看到 http://www.python.org/ 主页上“下载”两个字是否感觉特别异样啊。
Update: 抱歉该图片丢了。
Python 的下载页一直被墙…可能和GAE有关?<- 个人猜测
#Update1402: 新版 Python 主页上线了!链接变了,已解决此问题。
]]>今天无意中遇到了个便宜的墙内虚拟主机,速度不错,稳定性未知..
反正GAE最近也都不能访问..寒假又蛋疼没事做..就迁过去吧..
等那边差不多打理好了,就把这边转接过去…
最好是能文章对文章地转向..不过这就要现学现卖Python了…
正当我想将此Blog迁出去的时间,忽然发现GAE又恢复了..
Google了一下,应该是两天前刚恢复的:
2 天前 – 原来GAE复活了呀. … 原来GAE复活了呀 6:26 AM Dec 14th via 詹氏墙外后援会 · Dr_zhan. 詹. Footer. © 2010 Twitter; About Us · Contact · Blog · Status …
twitter.com/Dr_zhan/status/14687583015542786 – 网页快照
2 天前 – gae复活了,哦耶. … gae复活了,哦耶 3:11 AM Dec 13th via 撬墙脚 from here …
https://twitter.com/liveGZ/status/14276127841320961 – 网页快照
复活前试了下用 FreeWebHostingArea.com 和 000webhost.com 的免费空间架 WordPress。
两者速度都差不多,Ping>300ms,凌晨更悲剧,常常>400ms。只能用个简洁的主题,图片一多就受不了了。
还是GAE好啊,我这 Ping 在150毫秒左右,对于免费的来说,速度已经相当不错了。
只是没有像 WordPress 这类强大的程序。
好在 Micolog 一直在进步,7号发布了 Micolog 0.74,我刚才注意到,有空升级上来,更新说明。
忽然遇到了一个被和谐了的提问,显示:
“根据本站服务条款,天涯在线删除了本页部分内容。”
(没错,Beta版就是Beta版…上面还写着“天涯在线”…)
大惑不解,什么提问这么牛X,居然被和谐了..
于是提问道:“谷歌问答还是有河蟹的?”
几个小时后….
-_-|||| 我因为提问谷歌问答是否会和谐掉一些提问而被和谐了……
扣分(-10)扣威望(-2)就算了,还说别人发广告/恶意软件…太伤感情了…
Update100730:
昨天提问的“通过IPv6访问Youtube的My_Speed页面提示错误500?”也被删了,内容如标题一样,理由同样是 广告/恶意软件。
广告?帮Google做广告?恶意软件?YouTube是恶意软件啊….
两次都可以认为是说Google不好的..简直给人感觉就是,谷歌问答会和谐掉对自己不利的内容!
而且和谐的力度相当大…
Update100817:
由于 img.ly 被X,原图可能无法显示..已经重新传了一份在本地…现在不翻也可以看图了…
Error 101 (net::ERR_CONNECTION_RESET): Unknown error.
初步认为是被墙了..
所谓GAE就是 Google App Engine ,Google 提供的免费空间,用来放置网站等。
本Blog就是运行于GAE上的。
GAE提供了加密的SSL连接,所以不会被 GFω 监听(即关键词过滤)。
再加上GAE稳定,速度快,所有经常用来对付GFω…
也正因为次,GAE屡次被墙,但不久却又恢复了…
GAE和其他很多Google服务一样,都处于“半墙”状态..
看来现在这种状态又加深了…
联系到近期Google的搜索服务也启用了HTTPS加密连接…
怕是这项服务也不保了…
Ps. 请观看幻灯片——《什么是GFω》(转载),备用地址。
Update20100529: 今天晚上连HTTP都无法访问了,Ping也不通…
现在22点后测试,又都恢复了,包括HTTPS也恢复了…
先前那个免费空间速度太慢,而且是不是出现无法访问的现象..
为了稳定,想想还是迁到GAE好了…对于免费服务来说,还是它稳定(不被GFω的话)且速度快~
之前用的WordPress是用PHP写的,而GAE仅支持Python和Java..于是又要找新的Blog程序了..
先传了个Plog..功能太弱.. 后来换了这个Micolog,嗯,虽然功能和资源都不能和Wordpress比,但是还凑合。
官网上好看的主题还是有一些的~
另外一个大好功能是,支持导入和导出,而且用的都是Worlpress的格式。
传到GAE的时候出了几个小问题,上传过程中断开,再上传会提示:
Error 409: — begin server output —
要解决,执行:
appcfg.py rollback 程序目录
回滚即可。
还有一个大问题是,绑定GAE域名。
貌似几种方法,A记录,CHNAME记录,的N个IP都惨遭GFω黑手, 都需要挂代理才可访问…唉..
话说那些拿GAE做代理的,都低调些啊!!这样下去GAE会越来越不好使,甚至完全无法使用!
(现在Python官网的下载页面都被墙了…)
算了算了,不绑定了,就这样了。
]]>