网络 – 折腾(存档) https://blog.sorz.org 已停止更新和维护。该页面为2018年3月创建的存档,其内容可能已过于陈旧、与现状不符,仅作为历史存档用作参考。 Sun, 14 Jan 2018 11:08:20 +0000 zh-CN hourly 1 https://wordpress.org/?v=4.9.4 简单的 BT Tracker 连接检测 /p/bt-tracker-helper/ /p/bt-tracker-helper/#respond Sat, 24 Oct 2015 09:43:43 +0000 /?p=348077

RT @kgen: 大家 BT 下载的时候,不要开着 VPN,发达国家的版权投诉猛如虎。版权方会主动放出一些正版的 BT 种子,然后收集所有连接来源 IP,投诉或索赔。

这对于 VPN 提供商来说也是一个很头疼的问题。现在的 BT 客户端为了避免被封,已经经进化出了各种加密、混淆手段,直接检测 P2P 流量很困难。不过,大多数时候,客户端都在下载是都会同时连接一些 BitTorrent tracker 服务器。这些服务器数量有限且地址、端口号相对固定,可以非常容易地收集,并直接通过 IP 地址和端口号判断而不必对流量内容进行深度检测。探测到了就扔炸弹。是一个简单易行的判断用户是否正在使用 BT 的方法。

这个方法已在某家梯子站上部署了有一段时间了。鉴于它误报率(false postive rate)低但漏报率(false negtive rate)高,检测到的「处罚」会比较严厉——发现连接就立即挂断这条 VPN 连接。效果似乎还不错,仅有的几次投诉经过排查,发现都是在这套机制的某些环节失效时发生的。

当时为了方便操作,写了几个脚本。现在重新整理了一下,放在了 GitHub 上。这个脚本主要用于从 BT 种子文件里收集 trackers 的地址,然后解析域名得到对应的 IP 地址、协议和端口号。脚本本身并不进行检测等操作,只是为了方便配置防火墙而写。

示例

先在各处收集一些 BT 种子,这些种子文件中通常会包含一个或多个的 trackers。可使用./trackers.py torrent获得 trackers 的 URL 列表,重复的地址会被自动剔除:

$ ./trackers.py torrent *.torrent > trackers.txt

$ cat trackers.txt
> http://tracker.yify-torrents.com
> udp://tracker.justseed.it:1337
> udp://tracker.openbittorrent.com:80
> http://nyaatorrents.info:3277
  ......

防火墙通常需要匹配 IP 地址而非域名,并且一个域名可能对应多个 IP。这个脚本提供了解析 tracker URL 的功能,每个地址会被解析为一个或多个 IP 地址 – 协议 – 端口 的组合:

$ ./trackers.py raw trackers.txt
> 188.166.82.104 tcp 6881
> 94.23.217.90 udp 1337
> 179.43.146.110 udp 80
  ......

使用 ipset 来匹配这些连接是一个不错的选择。可以使用./trackers.py ipset直接生成 ipset 规则,这些规则可经由ipset restore导入系统:

$ ./trackers.py ipset trackers trackers.txt > ipset.rules

$ cat ipset.rules
> create -exist trackers hash:ip,port family inet
> add -exist trackers 188.166.82.104,tcp:6881
> add -exist trackers 94.23.217.90,udp:1337
> add -exist trackers 179.43.146.110,udp:80
> ...

# ipset restore -file ipset.rules

另外,虽然不是很推荐这么做,从收集 trackers 到导入 ipset 的操作也可以一步完成:

# ./trackers.py torrent *.torrent | ./trackers.py ipset trackers - | ipset restore

导入 ipset 后,可以使用 iptables 匹配这个 ipset,进行进一步操作。例如可使用iptables -j LOG记录下连接 trackers 的行为:

# iptables -N TRACKERS
# iptables -A FORWARD -m set --match-set trackers dst,dst -j TRACKERS
# iptables -A TRACKERS -m limit --limit 1/sec --limit-burst 10 \
  -j LOG --log-level info --log-prefix trackers
# iptables -A TRACKERS -j DROP

一些发行版本使用了 syslogd 管理日志,可以修改它的配置,将连接记录转发至其他程序进行进一步处理。例如文章开头所说的,通过日志中记录的源 IP 识别出对应的用户,然后将其断线。

]]>
/p/bt-tracker-helper/feed/ 0
HSTS Preloading – 让你的域名「嵌入」主流浏览器,一同发行 /p/hsts-preload-list/ /p/hsts-preload-list/#comments Thu, 19 Mar 2015 19:39:59 +0000 /?p=348035 有点标题党的味道,但确实有这种效果,比如我现在用的这个域名sorz.org目前可以在 ChromiumFirefox 的源代码中找到。理论上,它也会出现在 Safari 和新版的 IE 里[1]

当然「听起来好像很厉害」只是个副作用,其目的还是为了确保安全。

TL;DR – 如果你的网站也支持全站 HTTPS,可以考虑配妥 HSTS 后在此提交申请

(2015-11 更新)现在 Qualys 的 SSL Server Test 也会显示相关信息啦:
ssllabs-result-with-hsts

一切为了安全

现代浏览器在安全上真是做足了功夫。

HTTPS

SSL 协议在早在上世纪末就已提出[2]。目前广泛使用的 TLS 1.2 是它的改良版,在 2008 年正式发布[3]。他们可以在很大程度上,保证数据在 浏览器 与 网站服务器 间传输时的安全,保证他们不在传输过程中被监听或者修改。

但目前仍有大量网站是不提供 HTTPS (SSL/TLS)连接的 [4],或是只在部分页面提供。浏览器不知道哪些网站使用 HTTPS,用户也不一定知道。现在通常的做法是,浏览器先默认使用 HTTP 连接,如果服务器要求安全连接,再通过这个 HTTP 连接返回给浏览器一个「重定向」,让浏览器转而使用 HTTPS。

这样有一个问题,因为 HTTP 是不安全的,这个「重定向」就有可能被攻击者吞掉。然后攻击者一方面冒充服务器,使用 HTTP 与浏览器进行通讯;另一方面冒充浏览器,与服务器使用 HTTPS 建立连接。这就是所谓的 SSLstrip 攻击。

HSTS

为了解决这个问题,HTTP Strict Transport Security (HSTS, HTTP 严格传输安全) 孕育而出。这个 2012 年才发布的新玩意儿其实很简单,就是制订了一种方法,让服务器能够告诉浏览器:「我支持 HTTPS,今后使用它连接我」。

具体来说,它在增加了一个 HTTP 头 Strict-Transport-Security,里面指明网站至少支持 HTTPS 多长时间、是否包含子域名等。浏览器会缓存这条规则,今后即使是用户或者网页指定了http://,浏览器也会无视掉它,强制使用安全连接。

Preload List

但这无法阻止首次访问网站时受攻击(此时还没有 HSTS 规则的缓存呢),浏览器们并不满足于此,推出了 HSTS Prelod List(预加载列表?)。顾名思义,就是将使用 HSTS 的网站域名直接内置(hard-coded)进浏览器。只要是出现在列表中的域名,就统统使用 HTTPS 连接,让攻击者无缝可钻,安全性大提升。

部署

唔.. 介绍了这么多背景,进入正题,怎么做?首先当然是要做好全站的 HTTPS 支持,这个就略过不提了。然后是部署 HSTS。最后是提交申请,进入浏览器的 Preload List。

UPDATE 20150904: 如果你在使用 CloudFlare,进入 Dashborad,在 Crypto 找到并开启 HSTS 就可以了。

HSTS

添加 HSTS 支持其实很简单,修改一下 Web 服务器的配置,增加一个 HTTP 头就行[5]。随手 Google 到了一篇文章,大家可以参考一下。

举个例子,这个域名目前使用的是:

Strict-Transport-Security: max-age=15552000; includeSubDomains; preload

max-age=15552000告诉浏览器缓存这条规则 180 天;includeSubDomains对子域名也使用相同的规则;preload允许将这条规则内置进浏览器。

部署完毕后,可以用这个网站测试一下。如果存在其他安全相关问题,也会被检测出来。

Preload List

Duang! 现在可以提交申请啦(自备梯子):
https://hstspreload.appspot.com/

这个网站似乎是 Google 牵头做的,但不仅 Chrome,Firefox、Safari 和 IE 也都会包含这个列表。所以只要在这里提交一次就行啦。

提交前注意几点:

  • 需在全站启用 HTTPS(包括子域名),同时重定向所有 HTTP 流量至 HTTPS;
  • max-age必须大于 10886400 秒(18 周);
  • includeSubdomainspreload
  • 不能反悔的哦。

另外,他说这个申请是要经过人工审核的(not automatic nor assured … undergo a manual review),所以可能要花上几周的时间。具体到并入个浏览器代码中,可能需要更长的时间。

老实说,当初我看见「人工审核」便没报太大希望,提交后就忘了这事了。近几天才偶然发现已经申请通过并入了浏览器,才知道其实并没有什么门槛(虽然目前的列表只有两千行左右)。所以我也不知道我从申请到通过花了多久…

附注

[1] 会并入随 Windows 10 发布的那个 IE 上,MSDN 是这样说的。所以我是不是还可以更标题党一点,比如「与 Windows 一同发布」或者「嵌入 Windows」,233。
[2] Wikipedia – Transport Layer Security
[3] RFC 5246 The Transport Layer Security (TLS) Version 1.2
[4] 65% of top 1,000,000 websites by 2014. Source.
[5] 其实要完全符合标准的话,还要复杂一些。如果你也在使用 nginx,可以参考这条评论进行设置,会更规范一些。

]]>
/p/hsts-preload-list/feed/ 34
简单的 SSTP 服务器 /p/sstp-server/ /p/sstp-server/#comments Wed, 01 Oct 2014 10:48:23 +0000 https://sorz.org/?p=347963 SSTP (Secure Socket Tunneling Protocol) 是微软开发的一种 VPN 协议,用 SSL/TLS 加密 PPP 流量。参见 使用 SSTP 协议的 VPN 有什么优势和缺陷?

本来没怎么关注这个东西,支持少非主流,IP over TCP,也是因为一些奇怪的事突然有了这方面需求了,才开始了解它。

SoftEther

目前在 Linux,服务器似乎只有 SoftEther 这个实现。这软件也是挺拼的,把各种流行的非主流的 VPN 协议都给实现了一遍,跨平台,GPL。还提供傻瓜化的 GUI 管理,中日英三语界面,简直就是部署 VPN 的大杀器。只是一来太重,二来对 RADIUS 支持不全,不太符合需求。

DIY

因为内部走的是 PPP,如果只是调用 pppd 并转发数据,由 pppd 负责用户认证等麻烦事,自己实现一个目测也不是太复杂。加上微软的官方文档甚详,近来又一直想学一下 Twisted 但又不知如何入手,决定来写一个试试。

托 Twisted 的简单易用,挺顺利地初步完成了这个 SSTP 服务器。

代码放在 GitHub 和 PyPI 上了,sorz/sstp-server
PyPI

安装使用

供参考,详见sstpd --help

sudo apt-get install python-dev python-pip python-twisted
sudo pip install sstp-server
sudo sstpd -c cert.pem --local 10.0.0.1 --remote 10.0.0.0/24

关于证书,请参考 HTTPS 证书相关教程。
别忘了建/etc/ppp/options.sstpd,一个例子:

name sstpd
require-mschap-v2
nologfd
nodefaultroute
ms-dns 8.8.8.8
ms-dns 8.8.4.4

需要 IPv6 支持的,可加参数--listen ::

麻烦

折腾过程中遇到的主要障碍还是关于 PPP 的。简单地转发是不行的,因为每个 SSTP 包中只允许放入一个 PPP frame,需要自行分离出每个 frame。

PPP 这个古老的协议比预想中的要复杂。Windows 在 SSTP 中使用的其实是这个 HDLC-like Framing。找了一份 SSTP 客户端的代码,它是自行对两种格式进行了转换。好在后来发现 pppd 其实自身就支持 HDLC(sync 参数),于是成功偷懒

但后来发现 HDLC 似乎需要 Linux 内核支持(CONFIG_PPP_SYNC_TTY = yes)。

更新:
果然偷懒失败了,用 HDLC 取巧的方法是不行的,偶尔会有 frame 被截断,后面就全乱了。于是还是老老实实地照着 RFC 1662 来做 framing,自行 (un)escape 一些字符。但是这样性能变得很糟糕,试着把这部分用 C 扩展重写了一遍。想来这还是我第一次出于解决问题的需要写 C 呢……

感谢 @deba12 指出了这个问题,并协助测试、改善性能。

安全性

目前这个实现其实是不完整的,没有实现 Crypto binding 部分,导致其可能遭受中间人攻击。

使用了 SSL 还会遭受中间人攻击?微软在文档末尾提供了一个这种攻击的情境,挺有意思的。

攻击者建立一个假 Wi-Fi AP,然后诱骗用户连接。
Wi-Fi 使用 802.11 EAP 进行认证,用户以为他是在登录 Wi-Fi,但实际上,攻击者将这个认证请求转发给了 SSTP(PPP) 服务器!用户确实在和真的认证服务器在对话,只不过认证的不是 Wi-Fi 而是 SSTP 服务。

Crypto binding 可以防止这种攻击,想详细了解请参见微软文档。
但实现这个有些复杂,我这边的使用情境暂时没有这个需求,就先放一放了。
(懒你就直说 _(:з」∠)_

]]>
/p/sstp-server/feed/ 73
OpenWrt VPN 按域名路由 /p/openwrt-outwall/ /p/openwrt-outwall/#comments Thu, 22 May 2014 14:50:23 +0000 https://sorz.org/?p=347911 简述

这是一种基于域名的 VPN 智能翻越方案。不同于 chnroutes 这类通过维护一个 IP 地址列表来区分国内外网站的方案,基于域名的方式不受 IP 地址变动的影响。仅需维护一个相对很少有变化的域名列表即可。

dnsmasq 在 2.66 版之后加入了对 ipset 的支持,可将指定域名的 IP 解析后自动加入某一 ipset 中。 再配置路由规制,使该 ipset 中的 IP 走 VPN 即可。

感谢 @wzyboy 提供了此方案的思路。

大致流程

  • 配置 VPN
  • 配置 dnsmasq,指定域名
  • 增加一个路由表,默认网关为 VPN
  • 使用 iptables 匹配 ipset 并打上 mark
  • 使用 ip rule 将打上 mark 的包送入该路由表

所需软件

  • iproute2
  • dnsmasq (>= 2.66, has ipset)
  • iptables (with ipset and mark modules)

opkg update
opkg install ip ipset kmod-ipt-ipset dnsmasq-full

OpenWrt 默认的dnsmasq包并不包含 ipset 支持,需安装dnsmasq-full(感谢 @leavic 的提醒)。亦可修改 makefile 自行编译,以节省空间。

配置文件

列举一下涉及的各部分配置文件,供参考。

VPN

各类 VPN 均可,请参考 VPN overview。注意请将 VPN 设为默认路由。

rt_tables

添加outwall路由表。 走 VPN 的流量都将使用此表。

echo "200 outwall" >> /etc/iproute2/rt_tables

rc.local

使系统启动时创建一个名为outwall的 ipset。
在 /etc/rc.local 中添加:

# vi /etc/rc.local
ipset create outwall hash:ip

名字可以随便取,与 dnsmasq.conf 和 firewall.user 中的保持一致即可。

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 Post-connected Script

确保以下脚本在每次 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

这段脚本有三个作用:

  1. 让 8.8.8.8 走 VPN,防止 DNS 污染;
  2. 由于 VPN 断线时相关路由会被自动删除,所以在需要每次连接时添加一次;
  3. ip rule 在 VPN 断线时将手动删除(见下一节),切换至正常路由。

VPN Disconnect Script

见上节说明,须在 VPN 断线时 自动执行的脚本:

#!/bin/sh
ip rule del table outwall

dnsmasq.conf

请参考 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 结尾域名。

一个可供参考的配置如下, 该配置:

  • 尽可能地让 Google 所有常用服务走 VPN
  • 允许访问 Facebook、Twitter 和 YouTube
  • 让 GitHub 的 CDN 走 VPN
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

关于 Shadowsocks

(更新于 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) 等无法使用。

]]>
/p/openwrt-outwall/feed/ 22
半公开 Shadowsocks 架设注意事项 /p/deploy-ss/ /p/deploy-ss/#comments Thu, 17 Apr 2014 10:14:22 +0000 https://sorz.org/?p=347891 作为一个轻量级代理工具,安装配置很方便,基本上照着 README 做就行了。
这里说下在 Linux VPS 上架设半公开 Shdowsocks 服务可能需要用到的两项额外配置

同样由于为了保持轻量,Shadowsocks(这类)工具并不提供访问控制(ACL)和流量控制(TC)功能。 这在某些情景下可能产生安全隐患:公开或半公开帐号,可能有不信任的人使用;服务器上同时运行了其他服务。

禁止访问本机与内网

在 Linux 中,使用 TCP/UDP 在进程间通讯十分常见,比如 MySQL、PHP-FPM、Memcached… 它们在 127.0.0.1 上开一个端口,让其他程序连接。

localhost 只有本地应用才可访问,所以一般认为是安全的。但挂上 Shadowsocks 后,在客户端发起的请求,在系统看来,就是本地应用发出的。

感谢 @lilydjwg 的提醒,可以这么做:

Step 1  以outwall用户身份运行 Shadowsocks

su -c "ssserver -c /path/to/config.json" outwall

或者,Ubuntu 用户可使用 Upstart 运行:

# /etc/init/ssserver.conf
description "Shadowsocks Server"
start on runlevel [2345]
stop on runlevel [016]
setuid outwall
setgid outwall
respawn
exec ssserver -c /path/to/config.json

(日志将保存在 /var/log/upstart/ 下)

再或者,更高大上的 systemd:不会

Step 2  用 owner 模块匹配进程 UID

iptables -A OUTPUT ! -o eth0 -m owner --uid-owner outwall \
    -j REJECT --reject-with icmp-host-prohibited

这样,Shadowsocks 就只能连接外网(eth0)了。

另外,如 @clowwindy 所言单机使用时用 Unix Socket 代替 TCP/UDP 个好主意

2015-03-12 更新:
Shadowsocks (Python) 最新版已可以通过--forbidden-ip IPLIST参数禁止访问指定 IP。

流量控制

之前用美帝的 VPS 就完全不用在意带宽问题.. 少说 100Mbps 多则 1Gbps 的口,不限也大丈夫… 换成港服后就蛋疼了…

试着用 tc/tcng 来做流量控制,似乎挺复杂的,我就说下大致思路吧:
将 SS 的上行流量分两类:发回墙内的 与 发给墙外的
前者优先级低于后者,两者均低于本机的其他服务。

发回墙内的,由于源端口总是为 SS 监听端口,所以直接在 tc 里匹配端口即可;
发给墙外的,端口不一,可用 iptables owner 匹配后打上 mark,再用 tc 匹配 mark。

iptables -t mangle -A POSTROUTING -m owner --uid-owner outwall
    -j MARK --set-mark 2

tc 略复杂啊,我觉得我没法讲清楚了(是你压根就不懂吧喂
有一个坑点需要注意,rate 有两重含义,一个是“保证带宽”,是一个确切的值。另一个是权重,可用带宽会按 rate 的比例分配。

2015-03-12 更新:
如果嫌 tc 麻烦,可使用 iptables 的hashlimit模块来实现限速。

]]>
/p/deploy-ss/feed/ 3
乱发数据包 /p/illegal-send/ /p/illegal-send/#comments Sun, 23 Mar 2014 10:43:58 +0000 https://sorz.org/?p=347865 这是去年写的一个用于干扰教学的工具。

Computer Networks 这门课,需要用到 Wireshark 这个抓包软件查看网络流量。
写了这玩意,通过广播一些特制的数据,可以在其他人的机子上刷出点有趣(?)的东西:在 Wireshark 上刷奇怪的东西

如图,一眼就能看出不正常.. 其实就是乱发 HTTP Response,并把原来显示状态码(200、404 …)的部分,改成了其他文字…

由于需要发,用了 WinPcap,它提供了一些 API 能够比较方便地跳过系统的协议栈,让网卡发送完全自定义的数据。这里使用了它的一个 Python 绑定 winpcapy

最后需要发送的是以太网帧,也就是说,要自己从 HTTP、TCP、IP 一路构建下来。为了简化工作使用了 dpkt ,里面提供了丰富的协议支持,能很方便地构建数据包。

一点小细节:Wireshark 会分析 TCP 协议,并把不正常的包用红色标出。为了避免这样,每个包的目的端口都是随机选取的,让 Wireshark 认为每个包都属于不同的 TCP 会话。当然换成随机的 IP 也没问题。

代码贴 Gist 上了 Gist 9721330

玩法:

  1. 下载安装 Python 2.7 和 WinPcap
  2. 下载打包的好的工具;
  3. 运行net_test.py得到网卡列表和对应的地址(e.g rpcap://\Device\NPF_{x-x-x-x-x});
  4. 运行net_test.py rpcap://\Device\NPF_{x-x-x-x-x}发送。
    完整用法是net_test.py <interface> [repeat_times [message]],可选重复次数与消息内容(默认是 5 次、发送随机表情)。

注意需要管理员权限,系统如有开 IP Forwarding 功能需关闭

代码不复杂,想玩更多花样可以自己改..
注意这样不按协议标准发数据的事.. 还是少做得好.. 玩玩就好,切勿滥用..

]]>
/p/illegal-send/feed/ 4
用 OpenVPN 实现双方 NAT 内 VPN 连接的尝试 /p/openvpn-traversal/ /p/openvpn-traversal/#comments Sun, 29 Sep 2013 05:37:41 +0000 https://sorz.org/?p=347754 学校位于墙外,宿舍宽带上下 100Mbps,如此优势不拿来架代理简直浪费.. 问题在于宿舍网络位于 NAT 后面没公网 IP,也没有 UPnP 这类方便的东西。不仅架代理麻烦,开 MC / TR 服务器什么的也很麻烦.. 尤其是在双方都是内网的情况下。

困难

于是做了不少尝试,比如宿舍内通过 VPN 连接自家路由器,然后将数据包通过自家路由器转发到宿舍路由器上,但这样做怎加了额外的延时,且受家里带宽限制。再比如通过 UDP 打洞 来实现双方直连..
但是如此一来,要用 UDP 来传送 TCP 内容,就需要自己实现 TCP 的各种功能,还要追踪连接,太过复杂大大超出自己能力水平(太弱了。所以就想着用各种办法偷懒。

绕开难点

上学期做的一个尝试是,用 tun 虚拟网卡,直接转发IP包。如此一来,TCP 全由系统实现,避开了这些复杂的事… (参见《OpenWrt 上使用 Python 操作 TAP/TUN》)最后止步于放假回家,和 Windows 下修改路由表的一些问题(很想具体喷一下但是这就扯远太了)。

前段时间忽然想到,OpenVPN 可使用 UDP 建立连接。如此一来,只要自己先(用少量代码)完成 UDP 打洞,而后的操作就可以全权交由(相当完善的) OpenVPN 处理,工作量、复杂度急剧下降。

具体实现

先简单重复一下 UDP 打洞的具体过程。

  1. 在 NAT 后的双方,向一台外网设备发送 UDP 包,让该设备得到双方的 外网 IP 和 UDP 端口号;
  2. 该外网设备通知双方对方的 外网 IP 和 端口号;
  3. 双方互相向对方的 外网端口 发送 UDP 包,连接建立;
  4. 完成,通过该连接通讯。

实际使用时可能会受到一些限制,宿舍网络不存在此问题不再累述。

 OpenVPN 服务器

首先,在宿舍内架 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 转发数据协助建立连接

然后,让 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)

↑ 让用户得到自己的 OpenVPN 客户端对应的外网端口。

# (...)

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

# (...)

↑ 这是 flask 的代码片段。通过 HTTP,将用户的外网地址端口发给下面的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')

↑ 将 flask 发来的用户地址端口信息,继续转发给 OpenVPN 服务器。也将后者的地址端口信息,储存至 memcache,方便 flask 读取。

OpenVPN 客户端请求连接

用户启动 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()

用户使用起来还是很方便的。给 Windows 用的话用可用 py2exe 打包,然后将 OpenVPN 客户端放在bin\ 。连接前只要安装 Tun/tap 驱动就行了。Linux 下把bin\openvpn.exe改成openvpn即可。在 Windows 7 和 Ubuntu 下测试过没问题。

问题

  • 受 NAT 的具体实现方式限制,部分网络无法使用( 在拜托 @ipchihin同学用宿舍网络测试时,就遇到了这种情况);
  • 某墙对 OpenVPN 的政策有时十分严格,甚至有报告因此被封 IP 的(1, 2, 3)。很难想像整个宿舍网络从此无法与国内联系的情形;
  • 连接不便。Windows 需安装 Tun/tap 驱动,Android 和 iOS 需要 root / 越狱。在连接层实现,提供 Socks 5 / HTTP 代理才是王道啊!

另外,因为只是一次尝试并未正式投入实用,所以通信时并未做认证和加密。为避免安全隐患,代码发布前经过少量修改,但修改后未经实际测试。如有疏漏请留言,谢谢。

当时做得就挺乱的,现在讲得更乱,还请求轻喷。

]]>
/p/openvpn-traversal/feed/ 5
用 pcap_sendpacket 时别开 IP Forwarding /p/pcap-router/ /p/pcap-router/#comments Sun, 08 Sep 2013 10:53:15 +0000 https://sorz.org/?p=347746 至少在我 Windows 7 下是这样的,用 pcap_sendpacket() 时别开系统的 IP forwarding 功能。
否则的话.. 包不会直接被送出去,而是会被系统路由一次再发。发生这种情况时,发送一个包,在本机会抓到两个包:前者没问题;后者 TTL 减一,源 MAC 和 目的 MAC 可能会被修改。但只有后一个包会被送出本机。

IP forwarding 默认是关闭的,修改注册表项HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\IPEnableRouter开启或关闭。启用后系统就成了个路由器,会根据路由表转发目标IP不是自己的包。我也忘了当时为什么开启来了。

不知道这是个例还是 WinPcap 的 bug ,别告诉我这是个 feature 就行…

]]>
/p/pcap-router/feed/ 3
GFWList 兼容 Squid /p/squid-blacklist/ /p/squid-blacklist/#comments Tue, 14 May 2013 16:39:25 +0000 http://sorz.org/?p=347666 这两天在玩 Squid,一个功能颇为丰富的网络代理(缓存)软件。
GFWList 是一个 AutoProxy 维护的一个列表,顾名思义。

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);其余先尝试直接访问,如果被重置,将会转而尝试上级代理。挺智能的。

]]>
/p/squid-blacklist/feed/ 2
OpenWrt 上使用 Python 操作 TAP/TUN /p/openwrt-py-tun/ /p/openwrt-py-tun/#comments Sat, 27 Apr 2013 18:58:40 +0000 http://sorz.org/?p=347635 这两天折腾 tun,之前完全没接触驱动这么底层的东西,全靠 GlacJAY 大大的这篇文章入门了。

一句话,参照此 gist,同时将TUNSETIFF = 0x400454ca改为TUNSETIFF = -2147199798 即可。

那篇文章的代码在 Windows 和 Ubuntu 下都正常运行,但今天在路由器上运行时却报错了:

Traceback (most recent call last):
  ……
  File "/root/movpn/tun.py", line 218, in _open_tun
    fcntl.ioctl(tun, TUNSETIFF, ifr)
IOError: [Errno 81] File descriptor in bad state

因为 vtun 运行正常,所以 tun 本身应该是没问题的。又测试了一下,这个错误在 ioctl 的request不正确时出现。遂铺天盖地地寻找此值,发现不少相同遭遇,如这里还有这,但没找到解决方案。

先去解了 ioctl,得知第二个参数是由好几个部分组成的,从 if_tun.h 找到了 ioctl.h,最后卡在一点C语言也不会,琢磨半天也没弄明白 TUNSETIFF 取值多少..

无奈,只好试着自己写段代码把 TUNSETIFF 打出来了..
之前只听说过 交叉编译 这个词,现在就要动手了好鸡冻,好在 OpenWrt 的相关资料很丰富:参照 wiki 以及 这篇帖子 外加一晚上时间终于完成了。

找了一个 C Hello Word 改了一下:

#include 
#include <linux/if_tun.h>
#include <sys/ioctl.h>

main() {
    printf("TUNSETNOCSUM  = %d\n", TUNSETNOCSUM);
    printf("TUNSETDEBUG   = %d\n", TUNSETDEBUG);
    printf("TUNSETIFF     = %d\n", TUNSETIFF);
    printf("TUNSETPERSIST = %d\n", TUNSETPERSIST);
    printf("TUNSETOWNER   = %d\n", TUNSETOWNER);
    printf("TUNSETLINK    = %d\n", TUNSETLINK);
    printf("TUNSETGROUP   = %d\n", TUNSETGROUP);
}

交叉编译:

xx@xx:~/openwrt/staging_dir/toolchain-mips_r2_gcc-4.6-linaro_uClibc-0.9.33.2$ \
    bin/mips-openwrt-linux-gcc test.c

上传路由器,执行:

TUNSETNOCSUM  = -2147199800
TUNSETDEBUG   = -2147199799
TUNSETIFF     = -2147199798
TUNSETPERSIST = -2147199797
TUNSETOWNER   = -2147199796
TUNSETLINK    = -2147199795
TUNSETGROUP   = -2147199794

OK,全出来了,话说为啥都是负数 = = 和 CPU 位宽整数长度啥的有关?换一个路由器是否还是这个值?
不知道,管用就行,Python 里TUNSETIFF = -2147199798就好了。

P.s. 嘛其实回想起来也不复杂,就是编译 OpenWrt 略显蛋疼:经常在各种下载时卡住,挂VPN、断VPN,反复试。 有个 git clone 就是卡在 84% 死活过不去,最后把 git:// 改成 https:// 才好..

]]>
/p/openwrt-py-tun/feed/ 6