7
23
2016
0

发包太快,请勿跟踪

之前写的那个处理 DNS AAAA 的程序,后来请求量大的时候就经常报错。经过研究,是在sendto的时候返回了「Pemission Denied」错误。后来的 Rust 版本也发生了类似的问题,得到操作系统返回的代码「EPERM」。

我翻了半天 man 手册,其中只说到向广播地址发包可能会得到 EACCES 错误。Google 也没有得到结果(都是些权限不够的问题,但我的程序是 root 跑的呀,并且错误比较零星)。后来发到 shlug 邮件列表中询问,才终于得知了和我有同样问题的人,但是也没有结论,只是说关掉 iptables 就正常了。可我的程序依赖 iptables 呢……而且我要的不仅仅是解决方案(实际上这个问题并没有造成什么可感知的影响,就算有,我也有办法 migrate),我更想知道为什么。

确定是发包太快造成的问题,拿着相关关键词去搜,还真找到了一些有用的信息。比如之前看过的 CloudFlare 低延迟 UDP 实验时会让 iptables 不跟踪相关数据包,有人在使用 SIP 协议时也遇到了同样的问题,并且在内核日志的帮助下解决了。于是我照着做,让 conntrack 放过我发出的 UDP 包:

iptables -t raw -I OUTPUT -p udp -m udp --sport 53 -j NOTRACK

然后不仅那些错误都没了,而且处理速度快了一倍!(图中红虚线是发生错误的时候。)

程序统计信息

Category: 网络 | Tags: iptables linux UDP
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
10
13
2013
4

通过 UDP 打洞连接 NAT 后边的 mosh

又是一篇关于 UDP 打洞的文章。之前写过关于在完全圆锥型(full cone)NAT的文章中如何使用 socat 命令打洞。根据那篇文章里的知识,连接到一个 full cone NAT 后边的 mosh 不成问题。不过,我现在的网络是受限圆锥型(restricted cone)NAT 了呢!

也就是复杂了一些。双方要向中间服务器和对方都发送数据包才可以。另外就是,客户端(mosh-client)这边得使用在打洞期间使用的端口号才行。

打洞流程根据维基百科,双方通过中间服务器(还是我的 udpaddr 啦)交换地址,双方均向得到的地址发送一数据包,然后开始正常通讯。比较麻烦,于是有了这个脚本

#!/usr/bin/env python3

import socket
import re
import sys
import subprocess

udp_server = ('xmpp.vim-cn.com', 2727)
addr_re = re.compile(r"\('(?P<ip>[^']+)', (?P<port>\d+)(?:, (?P<cport>\d+))?")

def main(server):
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  sock.settimeout(2)
  print('Send message...')
  sock.sendto(b'req from holepunch.py\n', udp_server)
  msg, addr = sock.recvfrom(1024)
  print('Got answer from %s: %s' % (addr, msg))
  m = addr_re.search(msg.decode())
  if not m:
    print("Error: can't parse answer.")
    sys.exit(1)
  m_ip = m.group('ip')
  m_port = int(m.group('port'))
  port = sock.getsockname()[1]
  print('Got my IP and Port: (%r, %s, %s).' % (m_ip, m_port, port))

  msg = input('> Peer address: ')
  m = addr_re.search(msg)
  if not m:
    print("Error: can't parse input.")
    sys.exit(2)
  p_ip = m.group('ip')
  p_port = int(m.group('port'))
  c_port = int(m.group('cport'))

  print('send initial packet and wait for answer...')
  sock.sendto(b'HELO\n', (p_ip, p_port))
  try:
    msg = sock.recvfrom(1024)
    print('Received:', msg)
  except socket.timeout:
    print("Timed out (it's normal).")

  if server:
    sock.close()
    print('Starting mosh server...')
    msg = subprocess.check_output(['mosh-server', 'new', '-p', str(port)])
    secret = msg.split()[3].decode()
    print('Connect with:\nMOSH_KEY=%s MOSH_CPORT=%s mosh-client %s %s' % (secret, c_port, m_ip, m_port))
  else:
    print('done.')

if __name__ == '__main__':
  server = len(sys.argv) == 2 and sys.argv[1] == '-s'
  main(server)

如果 mosh 服务器端位于受限 NAT 后,还需要给 mosh-client 打个(我随手写的很 dirty 的)补丁以便指定客户端使用的 UDP 端口号:

diff --git a/src/network/network.cc b/src/network/network.cc
index 2f4e0bf..718f6c5 100644
--- a/src/network/network.cc
+++ b/src/network/network.cc
@@ -176,6 +176,11 @@ Connection::Socket::Socket()
     perror( "setsockopt( IP_RECVTOS )" );
   }
 #endif
+
+  if ( getenv("MOSH_CPORT") ) {
+    int port = atoi(getenv("MOSH_CPORT"));
+    try_bind( _fd, INADDR_ANY, port, port);
+  }
 }

 void Connection::setup( void )

然后,连接流程如下:

  1. mosh 服务端运行holepunch.py -s命令,客户端运行holepunch.py
  2. 双方看到自己的地址信息(依次是IP, 外网端口, 本地端口)后,复制并发送给对方;
  3. 双方输入对方的地址。为了节省时间(mosh-server 只会等一分钟,NAT 上的端口映射也是有时效的),只要输入的一行内包含上述地址信息的文本即可,前后可以有不小心复制过来的多余字符;
  4. 服务端将得到一行包含 mosh 的密钥的命令。将此命令发送对客户端;客户端运行此命令连接。如果 mosh-client 的名字不是标准名字,需要自行修改;
  5. 一切顺利的话就连接上啦!

mosh 服务端输出示例:

>>> holepunch.py -s
Send message...
Got answer from ('202.133.113.62', 2727): b"Your address is ('180.109.80.47', 3169)\n"
Got my IP and Port: ('180.109.80.47', 3169, 8127).
> Peer address:  Port: ('222.95.148.73', 5223, 55473). 
send initial packet and wait for answer...
Timed out (it's normal).
Starting mosh server...

mosh-server (mosh 1.2.4)
Copyright 2012 Keith Winstein <mosh-devel@mit.edu>
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

[mosh-server detached, pid = 5202]
Connect with:
MOSH_KEY=1apguNqtfN/K4JSvllnJxA MOSH_CPORT=55473 mosh-client 222.95.148.73 5223

mosh 客户端输出示例:

>>> holepunch.py
Send message...
Got answer from ('202.133.113.62', 2727): b"Your address is ('222.95.148.73', 5223)\n"
Got my IP and Port: ('222.95.148.73', 5223, 55473).
> Peer address: rt: ('180.109.80.47', 3169, 8127)
send initial packet and wait for answer...
Timed out (it's normal).
done.

2013年10月20日更新:想要通过打出来的 UDP 洞进行 TCP 通信吗?参见文章通过 OpenVPN 让 TCP 使用 UDP 洞

Category: 网络 | Tags: python 网络 mosh UDP
8
28
2012
10

UDP打洞实验

两台没有外网 IP、在 NAT 后边的主机如何直连?UDP打洞通常可行,但是需要第三方服务器。方法如下:

在服务器 S 上监听一个 UDP 端口,在收到 UDP 数据包后把源地址发回去。代码如下(github):

import sys
import time
import socket

def main(port):
  s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  s.bind(('', port))
  try:
    while True:
      data, addr = s.recvfrom(4096)
      back = 'Your address is %r\n' % (addr,)
      s.sendto(back.encode(), addr)
      print(time.strftime('%Y-%m-%d %H:%M:%S'), addr, 'just sent us a message:', data.decode('utf-8', 'replace'), end='')
  except KeyboardInterrupt:
    print()

if __name__ == '__main__':
  try:
    main(int(sys.argv[1]))
  except (ValueError, IndexError):
    sys.exit('which port to listen?')

主机 A 发送数据包:

$ socat readline udp:xmpp.vim-cn.com:2727,sourceport=4567
my addr?
Your address is ('a.b.c.d', 40060)

输入任意消息并回车,一个 UDP 就从本地的 4567 发送出去了。从上述示例我们可以看到,NAT 设备转发时是从 40060 端口发送出去的。为了让服务器返回的数据能够到达内网主机,在一段时间内,NAT 设备会记住外网来自 40060 端口的 UDP 数据包要发送给主机 a.b.c.d 的 4567 端口。完全圆锥型NAT不会在意外部数据包是从什么地方发回来的。受限圆锥型NAT会忽略掉其它主机的数据包,上例中只认可来自 xmpp.vim-cn.com 的数据包。端口受限圆锥型NAT更进一步地要求源端口(上例中是 2727)必须跟之前发出的数据包的目的端口一致。当然,「之前发出的数据包」不必是最后一个。所以,除了最后一种——对称NAT——之外,其它类型的NAT都是有可能成功穿透的。参见维基百科条目网络地址转换STUN

后来通过 pystun 程序,我得知我所处的 NAT 是完全圆锥型的。

在知道 A 的发送地址后,主机 B 就可以向这个地址发送数据了。接下来的操作使用 socat 命令就是:

# host A
$ socat readline udp-listen:4567
# host B
$ socat readline udp:A:4567

然后 B 先发送数据让 A 知道 B 的地址(socat 会 connect 到这个地址),双方就可以相互通信了。当然,因为是 UDP 协议,所以通信是不可靠的,丢包啊乱序啊都有可能。

2013年10月13日更新:想要连接到 NAT 后边的 mosh 请看这里

Category: 网络 | Tags: python 网络 socat UDP
12
15
2010
4

从UDP到解决DNS污染的脚本

最近做网络课的实验,涉及UDP协议。因为UDP协议比TCP用得少,所以我以前没试过创建几个UDP的socket。现在忽然有了兴致,就试了试。

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

socket创建好了,往哪里发消息呢?我想到了DNS。首先要有报文。没找到容易上手的Python库,就Google到了这个,DNS报文的格式。没功夫细细研究,先弄个报文测试下:

q = b'>:\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07twitter\x03com\x00\x00\x01\x00\x01'
s.sendto(q, ('8.8.8.8', 53))

\x07twitter\x03com\x00就是要查询的域名了,我想测试什么,明白人都看得出来了。接下来接收回答,接收了多次:

>>> '.'.join(str(int(i)) for i in s.recv(4096)[-4:])
'46.82.174.68'
>>> '.'.join(str(int(i)) for i in s.recv(4096)[-4:])
'128.121.146.100'

再接收的话就阻塞了。对于普通域名,当然是最多只会接收到一次的啦。如果报文出错的话(比如我不小心少写了最后的\x00\x01),是收不到回答的。

有人依据此现象写了个pydnsproxy 放在Google Code上。主页说“暂不公布方法”,但其实很简单。下面是我整理过的代码,转成了Python3的语法:

#!/usr/bin/env python3
# vim:fileencoding=utf-8

from socketserver import *
from socket import *
import sys, os

'''
来源 http://code.google.com/p/pydnsproxy/
'''

DEF_LOCAL_HOST = '127.0.0.1'
DEF_REMOTE_SERVER = '8.8.8.8'
DEF_PORT = 5350
DEF_CONF_FILE = 'dnsserver.conf'
DEF_TIMEOUT = 0.4

gl_remote_server = None

class LocalDNSHandler(BaseRequestHandler):
  def setup(self):
    global gl_remote_server
    if not gl_remote_server:
      remote_server = DEF_REMOTE_SERVER
    else:
      remote_server = gl_remote_server
    self.dnsserver = (remote_server, 53)

  def handle(self):
    data, socket = self.request
    rspdata = self._getResponse(data)
    socket.sendto(rspdata, self.client_address)

  def _getResponse(self, data):
    "Send client's DNS request (data) to remote DNS server, and return its response."
    sock = socket(AF_INET, SOCK_DGRAM) # socket for the remote DNS server
    sock.sendto(data, self.dnsserver)
    sock.settimeout(5)
    while True:
      try:
        rspdata = sock.recv(4096)
        break
      except error as e:
        if e.errno != 11:
          raise
        else:
          print("Try again")
    # "delicious food" for GFW:
    while True:
      sock.settimeout(DEF_TIMEOUT)
      try:
        rspdata = sock.recv(4096)
        print("GFWed?")
      except timeout:
        break
      except error as e:
        if e.errno != 11:
          raise
        else:
          print("Trying again")
    return rspdata

class LocalDNSServer(ThreadingUDPServer):
  pass

def main():
  global gl_remote_server
  try:
    if hasattr(sys, 'frozen'):
      dir = os.path.dirname(sys.executable)
    else:
      dir = os.path.dirname(__file__)
    confFile = os.path.join(dir, DEF_CONF_FILE)
    f = open(confFile, 'r')
    dns = f.read().split('=')
    f.close()
    if len(dns) == 2:
      if dns[0].strip().lower() == 'dns':
        gl_remote_server = dns[1].strip()
      else:
        pass
  except:
    pass
  dnsserver = LocalDNSServer((DEF_LOCAL_HOST, DEF_PORT), LocalDNSHandler)
  dnsserver.serve_forever()

if __name__ == '__main__':
  main()

注意到和原程序不同的是,我捕获了错误号为11的socket.error异常。这个是EAGAIN,“资源临时不可用”,只会在设置了超时后出现。man文档recv(2)对此的解释是:

EAGAIN or EWOULDBLOCK
       The socket is marked  nonblocking  and  the  receive  operation
       would  block, or a receive timeout had been set and the timeout
       expired before data was received.  POSIX.1-2001  allows  either
       error  to be returned for this case, and does not require these
       constants to have the same value,  so  a  portable  application
       should check for both possibilities.

不知道是怎么回事,再次接收却又可以收到数据。也许正如其名,是期待调用者再次尝试吧。我越来越觉得,Python 的异常处理里应该有tryagain这样的语句了。

2012年11月10日更新:更好地摆脱 DNS 污染,请参考此文

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

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