5
27
2014
13

纯真 IP 数据库 QQWry 解析库 Python 3 版

这东西挺好用的,可惜我只寻到一多年以前的 Python 2 版本的,作者是 AutumnCat,不认识。但注释里提到的修改者 bones7456 是鼎鼎大名的骨头兄,现其博客已经长草……

一直以来,我都是通过子进程调用来使用的,因为我写的代码是 Python 3 版,比如这个寻找文本里的 IP 地址并标记的 ipmarkup 脚本。配合 Python 3.2 加入的 functools.lru_cache,效率还不错的样子。但近期有大量 IP 需要查询,才感到每个 IP 都开个子进程的方式实在太慢。遂将其修改为 Python 3 版,并加入了些 Python 后来才流行的 idiom。

脚本还是扔到 winterpy 仓库里了。GPLv2 授权的。

2014年8月2日更新:增加了在线更新的功能,从此不需要 Wine 就能更新数据库啦 :-) 更新方法来自微菜。更新命令如下:

python3 -m QQWry update
Category: python | Tags: python IP地址
3
13
2014
4

Python 3 的 super() 和 __class__

子类里访问父类的同名属性,而又不想直接引用父类的名字,因为说不定什么时候会去修改它,所以数据还是只保留一份的好。其实呢,还有更好的理由不去直接引用父类的名字,参见 Python’s super() considered super! | Deep Thoughts by Raymond Hettinger

这时候就该 super() 登场啦——

class A:
  def m(self):
    print('A')

class B(A):
  def m(self):
    print('B')
    super().m()

B().m()

当然 Python 2 里 super() 是一定要参数的,所以得这么写:

class B(A):
  def m(self):
    print('B')
    super(B, self).m()

需要提到自己的名字。这个名字也是动态查找的,在这种情况下替换第三方库中的类会出问题。

super() 很好地解决了访问父类中的方法的问题。那么,如果要访问父类的父类(准确地说,是方法解析顺序(MRO)中位于第三的类)的属性呢?

比如,B 类是继承 A 的,它重写了 A 的 m 方法。现在我们需要一个 C 类,它需要 B 类的一些方法,但是不要 B 的 m 方法,而改用 A 的。怎么间接地引用到 A 的 m 方法呢?使用self.__class__肯定是不行的,因为 C 还可能被进一步继承。

从文档中我注意到,super 的实现是通过插入一个名为 __class__ 的名字来实现的(super 会从调用栈里去查找这个 __class__ 名字)。所以,就像文档里暗示的,其实可以直接在定义方法时访问 __class__ 名字,它总是该方法被定义的类。继续我们的单字母类:

class C(B):
  def m(self):
    print('C')
    # see the difference!
    print(__class__.__mro__)
    print(self.__class__.__mro__)
    __class__.__mro__[2].m(self)

class D(C):
  def m(self):
    print('D')
    super().m()

o = D()
o.m()

会得到:

D
C
(<class 't.C'>, <class 't.B'>, <class 't.A'>, <class 'object'>)
(<class 't.D'>, <class 't.C'>, <class 't.B'>, <class 't.A'>, <class 'object'>)
A

不过,PyPy 并不支持这个 __class__ 名字。

Category: python | Tags: Python
3
6
2014
5

使用 PyQt 转换网页到 PDF

代码很简单,功能也很简单 =w=

#!/usr/bin/env python3

import sys

try:
  from PyQt4 import QtWebKit
  from PyQt4.QtCore import QUrl
  from PyQt4.QtGui import QApplication, QPrinter
except ImportError:
  from PySide import QtWebKit
  from PySide.QtCore import QUrl
  from PySide.QtGui import QApplication, QPrinter

app = QApplication(sys.argv)

def done(status):
  p = QPrinter()
  p.setOutputFormat(QPrinter.PdfFormat)
  p.setOutputFileName('a.pdf')
  view.print(p)
  app.exit()

view = QtWebKit.QWebView()
view.load(QUrl('http://lilydjwg.is-programmer.com/'))
view.loadFinished[bool].connect(done)
# PySide does not have QApplication.exec
app.exec_()

注意:虽然没有图形界面,但是还是需要 X 连接……

Category: python | Tags: Python PyQt Qt
3
3
2014
17

《冰雪奇缘》之缘——A站弹幕 JSON 转 crt

都是《冰雪奇缘》惹的祸,不仅画面美仑美奂,音乐激荡人心,而且还玩多语言版本的主题曲

you-get 竟助纣为虐,把歌词给下回来了!只叹 soimort 没有好人做到底,留一个语义不详的 JSON 让我情何以堪。

好在 muzuiget 才识不凡,有码略释其义,遂如我光影、声音、文字三位一体之愿!

编成代码数十行,只为女王歌一曲:

#!/usr/bin/env python3

import json
import sys

def cmtjson_reader(f):
  data = json.load(f)
  for o in data:
    c = o['c'].split(',')
    m = o['m']
    # see
    # https://github.com/muzuiget/niconvert/blob/master/niconvert/libsite/acfun.py
    start = float(c[0])
    yield start, m

def format_time(t):
  s, ms = divmod(t, 1)
  s = int(s)
  ms = int(ms * 1000)
  m, s = divmod(s, 60)
  h, m = divmod(m, 60)
  return '%d:%02d:%02d,%03d' % (h, m, s, ms)

def crt_writer(f):
  i = 0

  fmt = '%d\n%s --> %s\n%s\n\n'
  try:
    while True:
      t, m = yield
      ts = format_time(t)
      if i != 0:
        f.write(fmt % (i, format_time(old_t), ts, old_m))
      i += 1
      old_t = t
      old_m = m
  except GeneratorExit:
    f.write(fmt % (
      i+1, format_time(t), format_time(t+2), m))

def main():
  w = crt_writer(sys.stdout)
  w.send(None)
  old_d0 = 0
  for d in cmtjson_reader(sys.stdin):
    if d[0] < old_d0:
      break
    w.send(d)
    old_d0 = d[0]

if __name__ == '__main__':
  main()

mplayer 君虽识得 UTF-8 之码,却不通 fontconfig 之术,部分文字沦为了下划线。又及。

Category: python | Tags: python
2
23
2014
11

让我们收养孤儿进程吧

稍微了解一点类 UNIX 系统的进程管理的都知道,当一个进程的父进程死亡之后,它就变成了孤儿进程,会由进程号 1 的 init 进程收养,并且在它死亡时由 init 来收尸。但是,自从使用 systemd 来管理用户级服务进程之后,我发现 systemd --user 管理的进程总是在它之下,即使进程已经 fork 了好几次。systemd 是怎么做到的呢?

对一个软件的实现有不懂的想了解当然是读它的源码了。这种东西可没有另外的文档,因为源码本身即文档。当然之前我也 Google 过,没有得到结果。在又一个全新的源码树里寻寻觅觅一两天之后,终于找到了这个:

        if (arg_running_as == SYSTEMD_USER) {
                /* Become reaper of our children */
                if (prctl(PR_SET_CHILD_SUBREAPER, 1) < 0) {
                        log_warning("Failed to make us a subreaper: %m");
                        if (errno == EINVAL)
                                log_info("Perhaps the kernel version is too old (< 3.4?)");
                }
        }

原来是通过prctl系统调用实现的。于是去翻 prctl 的 man 手册,得知PR_SET_CHILD_SUBREAPER是 Linux 3.4 加入的新特性。把它设置为非零值,当前进程就会变成 subreaper,会像 1 号进程那样收养孤儿进程了。

当然用 C 写不好玩,于是先用 python-cffi 玩了会儿,最后还是写了个 Python 模块,也是抓住机会练习一下 C 啦。有个 python-prctl 模块,但是它没有包含这个调用。

#include<sys/prctl.h>
#include<Python.h>

static PyObject* subreap(PyObject *self, PyObject *args){
  PyObject* pyreaping;
  int reaping;
  int result;

  if (!PyArg_ParseTuple(args, "O!", &PyBool_Type, &pyreaping))
    return NULL;
  reaping = pyreaping == Py_True;

  Py_BEGIN_ALLOW_THREADS
  result = prctl(PR_SET_CHILD_SUBREAPER, reaping);
  Py_END_ALLOW_THREADS

  if(result != 0){
    return PyErr_SetFromErrno(PyExc_OSError);
  }else{
    Py_RETURN_NONE;
  }
}

static PyMethodDef mysysutil_methods[] = {
  {"subreap", subreap, METH_VARARGS},
  {NULL, NULL}    /* Sentinel */
};

static PyModuleDef mysysutil = {
  PyModuleDef_HEAD_INIT,
  "mysysutil",
  "My system utils",
  -1,
  mysysutil_methods,
  NULL, NULL, NULL, NULL
};

PyMODINIT_FUNC PyInit_mysysutil(void){
  PyObject* m;

  m = PyModule_Create(&mysysutil);
  if(m == NULL)
    return NULL;
  return m;
}

编译之后,

>>> import mysysutil
>>> mysysutil.subreap(True)

然后开子进程,不管它 fork 多少次,都依然会在这个 Python 进程之下啦。

但是,这样子不太好玩呢。如果我登陆之后所有启动的子进程都在一个进程之下不是更有意思么?于是我打上了 Awesome 的主意,因为它支持运行任意的 Lua 代码嘛。于是我又给这个 prctl 调用弄了个 Lua 绑定。最终的版本如下:

#include<lua.h>
#include<lualib.h>
#include<lauxlib.h>

#include<sys/prctl.h>
#include<sys/wait.h>
#include<errno.h>
#include<string.h>
#include<signal.h>

static int l_setsubreap(lua_State * L){
  int reap;
  if(lua_isboolean(L, 1)){
    reap = lua_toboolean(L, 1);
  }else{
    return luaL_argerror(L, 1, "not a boolean");
  }
  if(prctl(PR_SET_CHILD_SUBREAPER, reap) != 0){
    return luaL_error(L, "prctl failed: %s", strerror(errno));
  }
  return 0;
}

static int l_ignore_SIGCHLD(lua_State * L){
  signal(SIGCHLD, SIG_IGN);
  return 0;
}

static int l_reap(lua_State * L){
  int pid, st;
  pid = waitpid(-1, &st, WNOHANG);
  lua_pushinteger(L, st);
  lua_pushinteger(L, pid);
  return 2;
}

static const struct luaL_Reg l_lib[] = {
  {"setsubreap", l_setsubreap},
  {"reap", l_reap},
  {"ignore_SIGCHLD", l_ignore_SIGCHLD},
  {NULL, NULL}
};

int luaopen_clua(lua_State * L){
  lua_newtable(L);
  luaL_setfuncs(L, l_lib, 0);
  return 1;
}

除了调用 prctl 外,还增加了显式忽略 SIGCHLD 信号,以及非阻塞地调用 waitpid 收割单个僵尸进程的函数,因为 Awesome 本身没处理子进程退出,我一不小心弄出了好几个僵尸进程……对了,那个 waitpid 要注意给弄成非阻塞的,不然一不小心就会出问题

用的时候就是这样子,可以写到rc.lua里,也可以在 awesome-client 里调用:

package.cpath = package.cpath .. ';/home/lilydjwg/scripts/lua/cmod/?.so'
clua = require('clua')
clua.setsubreap(true)
clua.ignore_SIGCHLD()

最终,我的进程树成了这样子:

htop-awesome-tree

可以看到,由 Awesome 启动的进程已经全部待在 Awesome 进程树之下了。systemd --user 是由 PAM 启动的,所以不在 Awesome 树下。但是,那些 dbus 的东西和 gconfd-2、at-spi 之类的是怎么回事呀……

1
15
2014
12

被 Tornado coroutine 对异常的异常支持坑了

>>> python -m this | grep -A1 -F Errors
Errors should never pass silently.
Unless explicitly silenced.

因为要捕获子进程的标准输出、标准错误以及退出状态码,用 callback 写会非常麻烦,因为三者全部完成才能进行下一步操作。而使用 Tornado 的 coroutine 就很方便了,示例如下:

from tornado.gen import coroutine, Task
from tornado.process import Subprocess

@coroutine
def run_cmd(cmd):
    p = Subprocess(
        cmd,
        stdout = Subprocess.STREAM,
        stderr = Subprocess.STREAM,
    )
    out, err, code = yield [Task(p.stdout.read_until_close),
                            Task(p.stderr.read_until_close),
                            Task(p.set_exit_callback)]
    return out, err, code
    # For Python below 3.3, use
    # raise Return((out, err, code))

yield 一个 Task(或者 Future)的列表的话,它们会并发执行,全部执行完毕之后才会返回到这个 yield 位置继续执行。简洁干净。(不过我要吐槽一下为什么必须传列表,传元组就不对……)

于是乎,调用各种外部命令的部分被我由一堆回调改成了 coroutine,除了 yield 关键字有些别扭外,整个代码可读性好多了 :-)

可是后来,发生了这样的一件事:通过日志能看到一个 coroutine 前边的代码执行了,而后边的代码却没有执行,中间也没有 yield 到别的地方去!看上去非常诡异。

恰好前些天刚好看到一很不错的 Python 调试器 pudb。于是去执行中断的地方打断点(import pudb; pu.db),然后单步跟踪。这才发现原来是中间有个语句抛出了异常,然后这个异常被 coroutine「吃掉」了……示例代码如下:

#!/usr/bin/env python3

from tornado.gen import coroutine
from tornado.ioloop import IOLoop

@coroutine
def two():
  print('two entered')
  1 / 0
  print('two leaving')

@coroutine
def one():
  print('one entered')
  yield two()
  print('one leaving')

if __name__ == '__main__':
  one()
  IOLoop.current().start()

结果是:

one entered
two entered

执行从发生异常的那个位置中断了,并且没有任何错误消息被记录。(PS: 要是在 coroutine 里使用 try...except 的话是能抓到它的。)

以「tornado coroutine exception」为关键字找到了这个以及这个。原来 coroutine 的异常是被它返回的那个 Future 对象「吃掉」了。如果是在 Tornado 的 HTTP 服务里(RequestHandler),Tornado 的 web 模块会处理并记录这种异常。然而我是在 web 模块之外使用的,所以得自己来处理了:

#!/usr/bin/env python3

from tornado.gen import coroutine
from tornado.ioloop import IOLoop

@coroutine
def two():
  print('two entered')
  1 / 0
  print('two leaving')

@coroutine
def one():
  print('one entered')
  yield two()
  print('one leaving')

def _future_done(fu):
  fu.result()

if __name__ == '__main__':
  fu = one()
  fu.add_done_callback(_future_done)
  IOLoop.current().start()

这样就能看到有异常发生了:

one entered
two entered
ERROR:concurrent.futures:exception calling callback for <Future at 0x7f286c0bcf90 state=finished raised ZeroDivisionError>
  ...
  File "t.py", line 9, in two
    1 / 0
ZeroDivisionError: division by zero

那个异常的 Traceback 很长很长。没有原生的良好的协程支持的代价吧,不知道 Python 3.4 的 asyncio 里会不会好一些。

2014年8月2日更新:asyncio 在遇到这种情况时会打印错误日志,参见文档

Category: python | Tags: python tornado coroutine
11
21
2013
8

虾米歌词下载、Python Requests 库,以及 HTTP Keep-Alive

Requests

这是我第二次用 Requests 了。上一次是个下小说的脚本。我已经不记得自己为什么路过了 httplib2,也路过了 urllib3,却最终买了 Requests 的账。也许是不喜欢 httplib2 那个 Google Code 的首页,也许是厌倦了 urllib* 这种名字。不过我想更多的是开门见山的首页,以及开篇那段让人无法拒绝的介绍:

Requests is an Apache2 Licensed HTTP library, written in Python, for human beings.

Python’s standard urllib2 module provides most of the HTTP capabilities you need, but the API is thoroughly broken. It was built for a different time — and a different web. It requires an enormous amount of work (even method overrides) to perform the simplest of tasks.

Things shouldn’t be this way. Not in Python.

相比之下,中文翻译太差了(而且已经陈旧了)。我在这里再译一个版本:

Requests 是使用 Apache2 许可证的 HTTP 库。用 Python 编写,为人类编写。

Python 标准库中的 urllib2 模块提供了你所需要的大多数 HTTP 功能,但是它的 API 烂出翔来了。它是为另一个时代、另一个互联网所创建的。它需要巨量的工作,甚至包括各种方法覆盖,来完成最简单的任务。

事情不应该是那样的,在 Python 世界里。

Requests 使用的是 urllib3,因此继承了它的所有特性。Requests 支持 HTTP 连接保持和连接池,支持使用 cookie 保持会话,支持文件上传,支持自动确定响应内容的编码,支持国际化的 URL 和 POST 数据自动编码。现代、国际化、人性化。相见恨晚

Keep-Alive

以前一直以为 Keep-Alive(连接保持)就是节省了 TCP 连接建立的时间。直到渐渐了解了 TCP 慢启动。直到自己偶然间亲自对比了一次。

使用 httrack 默认参数下载 PostgreSQL 9.3 文档,一千多个页面,29 分钟。后来才注意到 httrack 的 Keep-Alive 支持要写参数手动启用。

使用 wget,默认会使用 Keep-Alive 来复用已有连接。几乎同样的页面,只花了 12 分钟

urllib3 说 它使用 Keep-Alive 单连接从 Google 下载 15 个页面比使用 urllib 每次建立新连接快了一倍。我这里的结果比它的测试还要好呢。

也许,支持 Keep-Alive,是我的 nvchecker 使用 pycurl 要快很多的很重要的一个原因吧。

其它

可惜的是,Requests 不支持 Tornado 的异步调度框架。不过还好,那边我可以用 libcurl,虽然 API 不 Pythonic,至少 cookie 管理和连接管理都很健全。

对了,虾米歌词下载脚本还是在老地方,歌词的获取参考了 you-get 的代码。Requests 的作者 Kenneth Reitz 也有一些其它有意思的东西,包括之前我发现但没意识到是他做的的、用于 HTTP 客户端测试的 Httpbin 网站。

Category: python | Tags: python tcp http
11
10
2013
5

终止永远等待网络的程序——纠结的 getmail 不再纠结

我收取邮件一直用的是 getmail,然而它有个问题:在网络不好的时候会挂在 recv 系统调用上,等好几个小时都有可能。还好我用的 crond 是 dcron,它知道同一个任务,在上一次任务还没执行完时即使时间到了也不应该再次执行,省了我一堆 flock 锁。不过,这样子导致我收不到邮件也不行啊。

以前也研究过一次,看到 getmail 有设置 socket 的超时时间啊,没整明白。最近网络又老是抽风,而且相当严重,导致我得不断地用 htop 去看、去杀没有反应的 getmail 进程。烦了,于是一边阅读 getmail 源码,一边使用 strace 观察,再配合 iptables 这神器,以及 the Silver searcher,终于找到了问题所在。

原来,由于 Python 的 SSL 对非阻塞套接字的支持问题12,getmail 在使用 SSL 连接时会强制使用阻塞式的套接字(见代码getmailcore/_pop3ssl.py:39以及getmailcore/_retrieverbases.py:187)。也许 Python 2.7 已经解决了这个问题,但是看上去 getmail 还是比较关心 Python 2.3 和 2.4。不过就算是 SSL 支持不好,调用下alarm不要一直待在那里傻傻地等嘛……也许,大部分 getmail 用户很少遇到足够差的网络?

于是考虑 fetchmail。花了两三天的业余时间终于弄明白我的需求该怎么配置了:

set daemon 300
set logfile ~/etc/log/fetchmail.log

defaults proto pop3 timeout 120 uidl
keep fetchsizelimit 0 mda "procmail -f %T"

poll pop.163.com interval 2
username "username" password "password"

poll pop.gmail.com
username "username" password "password"
ssl

poll pop.qq.com interval 2016 # 7 days
username "username" password "password"

但结果就是,除了 GMail 好一点,我让它「对从现在起所收到的邮件启用 POP」就没太大问题之外,腾讯还好,没几封邮件。网易那边,几百封旧邮件全部拖回来了…………

其实这个问题也还好,毕竟是一次性的。可我看它的日志,又发现,它每次收到 GMail 时,都会打印有多少封邮件已读。难道说,它每次去收邮件时都要列出所有可以用 POP3 收取的邮件,然后挑出没有收取过的?想到如果是这样,以后它每次取邮件时都要先取几千上万条已读邮件列表……这不跟 Google App Engine SDK 操作数据库加 offset 时前边所有数据全部读一遍一样扯淡吗……

于是又回来折腾 getmail。其实就这么一个问题,解决了就好。本来是准备去学学ptrace怎么用的,结果忍不住了,直接拿 Python 调 strace 写了这个:

#!/usr/bin/env python3

'''wait and kill subprocess if it doesn't response (from network)'''

import os
import sys
import select
import tempfile
import subprocess

timeout = 60

def new_group():
  os.setpgrp()

def main(args):
  path = os.path.join('/dev/shm', '_'.join(args).replace('/', '-'))
  if not os.path.exists(path):
    os.mkfifo(path, 0o600)
  pipe = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
  p = subprocess.Popen(['strace', '-o', path, '-e', 'trace=network'] + args, preexec_fn=new_group)

  try:
    while True:
      ret = p.poll()
      if ret is not None:
        return ret
      rs, ws, xs = select.select([pipe], (), (), timeout)
      if not rs:
        print('subprocess met network problem, killing...', file=sys.stderr)
        os.kill(-p.pid, 15)
      else:
        os.read(pipe, 1024)
  except KeyboardInterrupt:
    os.kill(-p.pid, 15)

  return -1

if __name__ == '__main__':
  try:
    import setproctitle
    setproctitle.setproctitle('killhung')
    del setproctitle
  except ImportError:
    pass
  sys.exit(main(sys.argv[1:]))

Python 果然快准狠 ^_^

代码在 winterpy 仓库里也有一份

这还是我编程时第一次用到进程组呢。没办法,光杀 strace 进程没效果。嗯,还有非阻塞的命名管道


PS: 去 GMail 设置页看完那个选项的具体名字后离开,结果遇到这个:

你这是让我「确定更改」呢还是「取消取消更改」呢……

10
29
2013
3

不需要 root 权限的 ICMP ping

ICMP 套接字是两年前 Linux 内核新加入的功能,目的是允许不需要 set-user-id 和CAP_NET_RAW权限的 ping 程序的实现。大家都知道,set-user-id 程序经常成为本地提权的途径。在 Linux 内核加入此功能之前,以安全为目标的 Openwall GNU/*/Linux 实现了除 ping 程序之外的所有程序去 suid 化……这个功能也是由他们提出并加入的。

我并没有在 man 手册中看到关于 ICMP 套接字的信息。关于 ICMP 套接字使用的细节来自于内核邮件列表

使用 ICMP 套接字的好处

  1. 程序不需要特殊的权限;
  2. 内核会帮助搞定一些工作。

坏处是:

  1. 基本没有兼容性可讲;
  2. 需要调整一个内核参数。

这个内核参数net.ipv4.ping_group_range,是一对整数,指定了允许使用 ICMP 套接字的组 ID的范围。默认值为1 0,意味着没有人能够使用这个特性。手动修改下:

sudo sysctl -w net.ipv4.ping_group_range='0 10'

当然你可以直接去写/proc/sys/net/ipv4/ping_group_range文件。

如果系统不支持这个特性,在创建套接字的时候会得到「Protocol not supported」错误,而如果没有权限,则会得到「Permission denied」错误。

创建 ICMP 套接字的方法如下:

import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_ICMP)

它的类型和 UDP 套接字一样,是SOCK_DGRAM,不是SOCK_RAW哦。这意味着你不会收到 20 字节的 IP 头。不仅仅如此,使用 ICMP 套接字不需要手工计算校验和,因为内核会重新计算的。ICMP id 也是由内核填的。在接收的时候,内核会只把相应 id 的 ICMP 回应返回给程序,不需要自己或者要求内核过滤了。

所以,要组装一个 ICMP ECHO 请求包头很容易了:

header = struct.pack('bbHHh', 8, 0, 0, 0, seq)

这五项依次是:类型(ECHO_REQUEST)、code(只能为零)、校验和(不需要管)、id(不需要管)、序列号。

接收起来也简单,只要看一下序列号知道是回应自己发的哪个包的就行了。

这里是我的一个很简单的实例。

附注:Mac OS X 在 Linux 之前实现了类似的功能。但是行为可能不太一样。有报告校验和需要自己计算的,也有报告发送正确但是返回报文是乱码的。另,FreeBSD 和 OpenBSD 不支持这个特性。

Category: Linux | Tags: linux python 网络 ICMP
10
20
2013
5

通过 OpenVPN 让 TCP 使用 UDP 洞

上篇成功让 mosh 走 UDP 洞,连接上了在 NAT 后边的主机。然而,很多有用的协议都是走 TCP 的,比如能传文件的 ssh、访问我的 MediaWiki 的 HTTP。TCP 洞难打,于是在想,OpenVPN 可以使用 UDP 协议,那么把双方用 OpenVPN 连起来,不是可以想用什么传输层的协议都可以了吗!于是,有了新的脚本

与 mosh 相比,打洞部分主要的不同有:

  1. OpenVPN 的可配置性强,不需要 hack 即可让它绑定到需要的端口。
  2. OpenVPN 本身使用证书认证,因此把证书部分保存在客户端,余下的部分(包含双方使用的 IP 地址和端口号)可以通过打好的洞明文发送,不用怕被攻击。所以跑我这个脚本的话,当前工作目录要可写,以便保存双方即将使用的配置文件。
  3. OpenVPN 客户端会自动忽略对方发过来它不认识的配置信息,不用想办法避免。
  4. OpenVPN 需要 root 权限,因此脚本调用了 sudo,需要及时输入密码

在实验过程中也遇到了一些坑:

  1. Python 里没办法将已连接的 UDP socket「断开连接」,即将一个已经connect的 UDP socket 恢复到初始时可接收任意地址数据的状态。原本以为connect(('0.0.0.0', 0))可以的,结果客户端这边始终收不到服务端发送的 OpenVPN 配置信息。Wireshark 抓包看到内核收到数据后发了 ICMP Port Unreachable 错误之后才明白过来。
  2. MTU 的问题。默认值会导致刚开始传输正常,但随后收不到数据的情况。添加mssfix 1400配置解决。(其实这个 OpenVPN man 手册里有写。)
  3. 超时的问题。先是没注意到 OpenVPN 服务端说没有配置keepalive的警告,结果连接空闲几分钟之后,「洞」就失效了。加上keepalive 10 60解决。

配置中没有加默认路由,所以连接上之后唯一的效果就是,两个主机分别多出了同一网段的两个 IP 地址,相互间可以进行 TCP 通信了~~

对了,客户端连接时需要一个包含 OpenVPN 证书信息的文件,其格式为:

<ca>
# ca.crt 文件内容
</ca>

<cert>
# crt 文件内容(只需要 BEGIN 和 END 标记的那部分)
</cert>

<key>
# key 文件内容
</key>

PS: 有这个想法后不久,发现 None 已经做过类似的事情了。不过脚本有点多,是使用第三方服务器而不是像我这样手工交换地址的。

Category: 网络 | Tags: python 网络 OpenVPN UDP

部分静态文件存储由又拍云存储提供。 | Theme: Aeros 2.0 by TheBuckmaker.com