3
3
2019
16

使用 cgroups net_cls 来让 docker 走代理

我这里 docker hub 连不上或者连上了访问很慢,根本没法用。本来我常规代理的办法,要么是 proxychains,要么是用 iptables 代理特定的 IP 段。至于 docker 嘛,亚马逊的 IP 段那么多,它用到的域名我也不是很清楚,一点点加好麻烦。作为系统服务,用 proxychains 不仅得修改 systemd 服务配置,而且不知道会不会出什么幺蛾子。最近刚好在某个地方看到这一手,就试试啰。

其实用法很简单的。去 /sys/fs/cgroup/net_cls 下建立个目录,往 net_cls.classid 里写一个整数(支持十六进制的 0x 表示法),然后把 dockerd 的 pid 写到 cgroup.procs 里去。最后用 iptables 代理这部分流量即可。现在都用 443 端口啦,所以只要代理它便好,也避免影响了别的东西:

iptables -t nat -A OUTPUT -p tcp --dport 443 -m cgroup --cgroup 0x110001 -j REDIRECT --to-ports XXX

XXX 是 ss-redir 的端口啦。

注意不要把进程的 pid 往 tasks 文件里写。那里得写的是 task 的 id 而不是 process 的 id,也就是说(用内核的术语来说)是线程的 pid 而不是进程的 tgid(thread group id)。所以非要写 tasks 文件的话,得把 docker 所有的线程的 pid 都写进去才行。真是混乱呢……画个表格好了:

用户态 内核 相关系统调用
pid tgid getpid, kill
tid pid gettid, tgkill
process task group fork, clone without CLONE_THREAD
thread task clone with CLONE_THREAD

另外如果更新过内核的话,那句 iptables 有可能会找不到模块的。(所以更新内核之后还是重启一下以避免尴尬吧。)

Category: shell | Tags: iptables linux cgroups 网络 代理
7
16
2018
5

使用 iptables 透明代理 TCP 与 UDP

很早之前,我在《Linux「真」全局 HTTP 代理方案》中介绍了 redsocks 方案。不过它只处理了 TCP,并没有处理 UDP,DNS 也是采用强制 TCP 的方式来处理的,再加上它本身还要将请求转发到真正的代理客户端,延迟比较高。然后,还可以结合 Wi-Fi 分享 或者网络命令空间,玩点更有趣的。

首先要有支持的代理客户端,比如 ss-redir。这个就不用多介绍了,配置好、跑起来即可。以下假设此代理跑在 127.0.0.1 的 $PPROT 端口上。

然后,TCP 的代理设置。使用的是和 redoscks 一样的方案。这个比较简单,除了有一点需要注意:DNAT 到 127.0.0.1 时,需要设置内核选项net.ipv4.conf.all.route_localnet=1

最麻烦的是 UDP 的代理,使用的是 TPROXY。首先,需要把要走代理的数据包路由到本地。以下假设我们给要代理的数据包打上标签 1。那么执行:

ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100

那个 100 是路由表的编号,可以自己选一个喜欢的。

然后,对于转发流量(来自局域网或者另外的网络命名空间),直接把需要代理的数据包扔给 TPROXY 目标,并且打上对应的标签即可。而对于本地产生的流量,不仅要带有对应的标签,而且需要在 OUTPUT 链打上一个(与之前不同的)标签,触发 reroute check 才行。

最后,对需要代理的数据包设置 iptables 规则:

协议 来源 目标
TCP 本地 nat OUTPUT -j REDIRECT --to-ports $PPROT
转发 PREROUTING -j DNAT --to-destination 127.0.0.1:$PPROT
UDP 本地 mangle OUTPUT
PREROUTING
-j MARK --set-mark 1
-j TPROXY --on-port $PPROT --on-ip 127.0.0.1
转发 PREROUTING -j TPROXY --on-port $PPROT --on-ip 127.0.0.1 --tproxy-mark 1/1

比如来自网络命名空间或者局域网的 IP 段 192.168.57.0/24 全部走代理:

iptables -t nat -A PREROUTING -p tcp -s 192.168.57.0/24 ! -d 192.168.57.0/24 -j DNAT --to-destination 127.0.0.1:$PPROT
iptables -t mangle -A PREROUTING -p udp -s 192.168.57.0/24 ! -d 192.168.57.0/24 -j TPROXY --on-port $PPROT --on-ip 127.0.0.1 --tproxy-mark 1/1
Category: 网络 | Tags: linux 网络 iptables UDP
4
23
2017
15

UDP: 谁动了我的源地址?

最近 #archlinux-cn 又流行玩 teeworlds 了,然而我却连不上那个服务器。

情况很奇怪。我能 ping 通服务器 IP,TCP 连接也正常,UDP traceroute 也表现得很正常(对关闭端口能够完成,对开放端口会在最后一跳开始得到一堆星号),并且我连接的时候,服务器能看到我在连接。也就是说,TCP 和 ICMP 都正常,UDP 上行正常,下行出了状况。

难道是有防火墙?首先呢,我能连接其它服务器,说明我这边没有问题;大部分人能连接上服务器,说明服务器那边也没有问题。所以,问题出在路上。也确实有另外的北京联通用户连不上这个服务器。但是很奇怪啊,为什么单单只是这一个 IP 的 UDP 包丢失了呢?

于是继续试验。从最简单的开始,用 netcat / socat 尝试通讯。方向反过来,我监听,服务器那边连接。端口是我在路由器上做过端口映射的。结果是正常的。再来,服务器那边监听,我往那边发,果然我就收不到包了。按理说,UDP 双方是对等的,不应该换了个方向就出问题呀。难道是因为端口映射?Wireshark 抓包看到本地使用的端口号之后,在路由器上映射一下,果然就通了!

然后,我注意到了一件十分诡异的事情:虽然我和服务器能够通讯了,但是我的 Wireshark 上只显示了我发出去的包,却看不到回来的包!我抓包时按服务器 IP 做了过滤,所以,回来的包的源 IP 不是服务器的地址!

重新抓包一看,果然。服务器 IP 是 202.118.17.142,但是回来的包的源 IP 变成了 121.22.88.41……看起来这是联通的设备,在下行 traceroute 时能够看到有节点与它 IP 相似(121.22.88.1)。原来又是这著名的「联不通」又干坏事了 -_-|||

虽然 socat 接收 UDP 时不介意源 IP 变化了,但是 teeworlds 介意啊。并且 NAT 那边也会不知所措。所以,首先得告诉路由器把来自这个 IP 的 UDP 包全部扔给我:

ssh 192.168.1.1 iptables -I FORWARD -i ppp0.2 -p udp -s 121.22.88.41 -j ACCEPT

于是数据包有了。接下来是修正源 IP。我试过 SNAT,无效。这东西似乎只对本地发出的包有用?于是我又用 netfilter_queue 了。这东西很强大呢~一个简单的 Python 脚本搞定:

#!/usr/bin/env python3

from netfilterqueue import NetfilterQueue
from scapy.all import *

def main(pkt):
  p = IP(pkt.get_payload())
  # print('recv', p)
  p.src = '202.118.17.142'
  p.chksum = None
  p[UDP].chksum = None
  pkt.set_payload(bytes(p))
  # print('fixed to', p)
  print('.', flush=True, end='')
  pkt.accept()

conf.color_theme = DefaultTheme()
nfqueue = NetfilterQueue()
nfqueue.bind(1, main)
try:
  nfqueue.run()
except KeyboardInterrupt:
  pass

然后是 iptables 命令:

sudo iptables -I INPUT -s 121.22.88.41 -p udp -j NFQUEUE --queue-num 1 --queue-bypass

scapy 这个神奇的网络库在 Arch 官方源里叫「scapy3k」。Python 的 netfilterqueue 模块需要用我自己修改过的这个版本

2017年7月30日更新:Python 的依赖有点麻烦,所以我又写了个 Rust 版本,放在 GitHub 上了

Category: 网络 | Tags: linux python 网络 iptables Rust
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: linux iptables UDP
4
17
2016
6

用 nfqueue + Python 回复 IPv6 DNS 请求

发生了这么一件事:服务器访问某些 URL 时经常会花费好几秒的时间。重现并分析 strace 记录之后,发现是 DNS 的 AAAA 记录的问题。

情况是这样的:CentOS 6 默认启用了 IPv6,于是 glibc 就会同时进行 A 和 AAAA 记录查询。然后呢,上游 DNS 是运营商的,不给力,经常在解析 AAAA 记录时花费几秒甚至十几秒,然后超时或者返回个 SERVFAIL。

不过咱在天朝,也没有 IPv6 网络可用,所以就禁用 IPv6 吧。通过 sysctl 禁用 IPv6 无效。glibc 是通过创建 IPv6 套接字的方式来决定是否进行 AAAA 请求的。查了一下,禁用掉 ipv6 这个内核模块就可以了。可是,rmmod ipv6 报告模块正在使用中。

lsof -nPi | grep -i ipv6

会列出几个正在使用 IPv6 套接字的进程。重启服务,或者重启机器太麻烦了,也不知道会不会有进程会起不来……于是我想起了歪心思:既然是因为 AAAA 记录回应慢,而咱并不使用这个回应,那就及时伪造一个呗。

于是上 nfqueue。DNS 是基于 UDP 的,所以处理起来也挺简单。用的是 netfilterqueue 这个库。DNS 解析用的是 dnslib

因为 53 端口已经被 DNS 服务器占用了,所以想从这个地址发送回应还得用底层的方法。尝试过 scapy,然而包总是发不出去,Wireshark 显示 MAC 地址没有正确填写……实在弄不明白要怎么做,干脆用 RAW 套接字好了。man 7 raw 之后发现挺简单的嘛,因为它直接就支持 UDP 协议,不用自己处理 IP 头。UDP 头还是要自己处理的,8 字节,其中最麻烦的校验和可以填全零=w= 至于解析 nfqueue 那边过来的 IP 包,我只需要源地址就可以了,所以就直接从相应的偏移取了~(其实想想,好像 dnslib 也不需要呢~)

代码如下(Gist 上也放了一份):

#!/usr/bin/env python3

import socket
import struct
import traceback
import subprocess
import time
import signal

import dnslib
from dnslib import DNSRecord
from netfilterqueue import NetfilterQueue

AAAA = dnslib.QTYPE.reverse['AAAA']
udpsock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP)
PORT = 53

def handle_packet(pkt):
  s = time.time()
  try:
    ip = pkt.get_payload()
    # 28 = 20B IPv4 header + 8B UDP header
    dns = DNSRecord.parse(ip[28:])
    if dns.q.qtype == AAAA:
      ret = dns.reply()
      src = socket.inet_ntoa(ip[12:16])
      sport = struct.unpack('!H', ip[20:22])[0]
      p = ret.pack()
      # print(ret, p)
      checksum = 0
      p = struct.pack('!HHHH', PORT, sport, len(p) + 8, checksum) + p
      udpsock.sendto(p, (src, sport))
      pkt.drop()
    else:
      pkt.accept()
  except KeyboardInterrupt:
    pkt.accept()
    raise
  except Exception:
    traceback.print_exc()
    pkt.accept()
  e = time.time()
  print('%.3fms' % ((e - s) * 1000))

def main():
  nfqueue = NetfilterQueue()
  nfqueue.bind(1, handle_packet)
  try:
    nfqueue.run()
  except KeyboardInterrupt:
    print()

def quit(signum, sigframe):
  raise KeyboardInterrupt

if __name__ == '__main__':
  signal.signal(signal.SIGTERM, quit)
  signal.signal(signal.SIGQUIT, quit)
  signal.signal(signal.SIGHUP, quit)
  subprocess.check_call(['iptables', '-I', 'INPUT', '-p', 'udp', '-m', 'udp', '--dport', str(PORT), '-j', 'NFQUEUE', '--queue-num', '1'])
  try:
    main()
  finally:
    subprocess.check_call(['iptables', '-D', 'INPUT', '-p', 'udp', '-m', 'udp', '--dport', str(PORT), '-j', 'NFQUEUE', '--queue-num', '1'])

写好之后、准备部署前,我还担心了一下 Python 的执行效率——要是请求太多处理不过来就麻烦了,得搞多进程呢。看了一下,一个包只有 3ms 的处理时间。然后发现 Python 其实也没有那么慢嘛,绝大部分时候不到 1ms 就搞定了~

部署到咱的 DNS 服务器上之后,AAAA 记录回应迅速,再也不会慢了~

调试网络程序,Wireshark 就是好用!

PS: 后来有人告诉我改 gai.conf 也可以。我试了一下,如下设置并没有阻止 glibc 请求 AAAA 记录——它压根就没读这个文件!

precedence ::ffff:0:0/96  100

PPS: 我还发现发送这两个 DNS 请求,glibc 2.12 用了两次 sendto,但是 Arch Linux 上的 glibc 2.23 只用了一次 sendmmsg~所以大家还是尽量升级吧,有好处的呢。

Category: 网络 | Tags: python DNS iptables 中国特色
2
7
2014
27

Linux「真」全局 HTTP 代理方案

看到 ArchWiki 上 GoAgent 条目的亚全局代理方案,只是设置了代理相关环境变量。我就想,为什么不实现一个真正的全局 HTTP 代理呢?

最终,答案是:Linux 太灵活了,以至于想写一个脚本来搞定很麻烦。不过方案如下,有兴趣的可以折腾折腾。

首先,需要用到的工具:dnsmasq、iptables、redsocks,以及 HTTP 代理工具。dnsmasq 是用来缓存 DNS 请求的,iptables 把 TCP 流转接到 redsocks,而 redsocks 将 TCP 流转接到代理上。

最小 dnsmasq 配置如下:

listen-address=127.0.0.1
cache-size=500
server=127.0.0.1#5353
bogus-nxdomain=127.0.0.1

这里使用了本地的 dnscrypt 服务(假设其在 5353 端口上提供服务)。也可以使用国外服务器,只是需要更细致的配置来迫使其走 TCP。

iptables 命令如下:

# 创建一个叫 REDSOCKS 的链,查看和删除的时候方便
iptables -t nat -N REDSOCKS
# 所有输出的数据都使用此链
iptables -t nat -A OUTPUT -j REDSOCKS

# 代理自己不要再被重定向,按自己的需求调整/添加。一定不要弄错,否则会造成死循环的
iptables -t nat -I REDSOCKS -m owner --uid-owner redsocks -j RETURN
iptables -t nat -I REDSOCKS -m owner --uid-owner goagent -j RETURN
iptables -t nat -I REDSOCKS -m owner --uid-owner dnscrypt -j RETURN

# 局域网不要代理
iptables -t nat -A REDSOCKS -d 0.0.0.0/8 -j RETURN
iptables -t nat -A REDSOCKS -d 10.0.0.0/8 -j RETURN
iptables -t nat -A REDSOCKS -d 169.254.0.0/16 -j RETURN
iptables -t nat -A REDSOCKS -d 172.16.0.0/12 -j RETURN
iptables -t nat -A REDSOCKS -d 192.168.0.0/16 -j RETURN
iptables -t nat -A REDSOCKS -d 224.0.0.0/4 -j RETURN
iptables -t nat -A REDSOCKS -d 240.0.0.0/4 -j RETURN

# HTTP 和 HTTPS 转到 redsocks
iptables -t nat -A REDSOCKS -p tcp --dport 80 -j REDIRECT --to-ports $HTTP_PORT
iptables -t nat -A REDSOCKS -p tcp --dport 443 -j REDIRECT --to-ports $HTTPS_PORT
# 如果使用国外代理的话,走 UDP 的 DNS 请求转到 redsocks,redsocks 会让其使用 TCP 重试
iptables -t nat -A REDSOCKS -p udp --dport 53 -j REDIRECT --to-ports $DNS_PORT
# 如果走 TCP 的 DNS 请求也需要代理的话,使用下边这句。一般不需要
iptables -t nat -A REDSOCKS -p tcp --dport 53 -j REDIRECT --to-ports $HTTPS_PORT

redsocks 的配置:

base {
  log_debug = off;
  log_info = off;
  daemon = on; 
  redirector = iptables;
}
// 处理 HTTP 请求
redsocks {
  local_ip = 127.0.0.1;
  local_port = $HTTP_PORT;
  ip = $HTTP_PROXY_IP;
  port = $HTTP_PROXY_PORT;
  type = http-relay; 
}
// 处理 HTTPS 请求,需要一个支持 HTTP CONNECT 的代理服务器,或者 socks 代理服务器
redsocks {
  local_ip = 127.0.0.1;
  local_port = $HTTPS_PORT;
  ip = $SSL_PROXY_IP;
  port = $SSL_PROXY_PORT;
  type = http-connect;  // or socks4, socks5
}
// 回应 UDP DNS 请求,告诉其需要使用 TCP 协议重试
dnstc {
  local_ip = 127.0.0.1;
  local_port = $DNS_PORT;
}

然后以相应的用户和配置文件启动 dnsmasq 以及 redsocks。修改/etc/resolv.conf

nameserver 127.0.0.1

至于分流的事情,HTTP 部分可以交给 privoxy,但是 HTTPS 部分不好办。可以再设立一个像 GoAgent 那样的中间人型 HTTPS 代理,或者更简单地,直接根据 IP 地址,国内的直接RETURN掉。

以上就是整个方案了。有些麻烦而我又不需要所以没测试。反正就是这个意思。Android 软件 GAEProxy 就是这么干的(不过它没使用 iptables 的 owner 模块,导致我不小心弄出了死循环)。另外,BSD 系统也可以使用类似的方案。

12
8
2012
7

iptables 访问控制规则两则

防 ssh 暴力破解

一直以来,面对 Vim 显示的 auth.log 里满屏的红色 ssh 登录失败记录,要么容忍,要么换端口号,要么是 fail2ban。换端口号显然会造成很多不便,尤其是使用者比较多的时候。fail2ban 以前也用得挺好的,但是需要手工编辑配置文件,阅读其中长长的注释并且小心翼翼地修改参数。配置好之后还会经常收到 fail2ban 发出的邮件。这些都可以忍受。直到有一天,某位使用者不小心登录失败多次以后,那个 IP 被封掉了。我从 /etc/hosts.deny 中删除了对应的项目,但是没有用,因为 fail2ban 会去检查 auth.log,然后把那个 IP 给加回去……

前两天本来是寻找限速的命令的,却无意之中看到了防 ssh 暴力破解的命令,如下:

iptables -I INPUT -p tcp --dport 22 -i eth0 -m state --state NEW -m recent --set
iptables -I INPUT -p tcp --dport 22 -i eth0 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 -j DROP

第一句是说,对于外来数据,如果是 TCP 协议,目标端口号是 22,网络接口是 eth0,状态是新连接,那么把它加到最近列表中。第二句是说,对于这样的连接,如果在最近列表中,并且在 60 秒内达到或者超过四次,那么丢弃该数据。其中的-m是模块的意思。

也就是说,如果有人从一个 IP 一分钟内连接尝试四次 ssh 登录的话,那么它就会被加入黑名单,后续连接将会被丢弃。这是对付 ssh 暴力破解的绝佳规则了。不用修改 openssh,也不用另启一个容易招麻烦的服务。不过不知道多久以后那个 IP 才能重新连接上。

我实际使用时正有一北京 IP 在尝试 ssh 登录。命令执行后,auth.log 里的红色失败消息又出现了四次,然后就没有了。后来再查看时,虽然还是能看到不少红色,但是没有以前那么密集了。更重要的是,每四条登录失败消息间的时间间隔比较大了。可谓效果显著啊。

网络限速

这是我这次搜索 iptables 相关信息的本意。起因是这样子的,在本地测试的时候,经常会发现本地连接的速度实在是太快了。对于网站,不能反映其真实的使用体验;对于网络程序,无法测试其在网络不良时的表现,由于测试的规模小,一些真实使用时容易出现的竞态也由于操作完成得太快而无法重现。

很早就知道 iptables 能够对转发流量进行限速。既然是 iptables 而不是某些商业软件,它就没理由只能对外部流量而不对本地接口 lo 进行限速。于是最后弄到如下命令:

iptables -A INPUT -s 127.0.0.1 -p tcp -d 127.0.0.1 --dport 6900:6901 -m limit --limit 1/s -j ACCEPT
iptables -A INPUT -s 127.0.0.1 -p tcp -d 127.0.0.1 --dport 6900:6901 -j DROP

这两条规则组合起来是说,对于所有从 127.0.0.1 到同样的地址的 6900 到 6901 端口的 TCP 连接,每秒只接受一个数据包,多余的丢弃。后边那句是必要的,如果不写的话就没作用了,因为默认策略是接受。

要注意的是,如果使用域名localhost的话,很可能会使用 IPv6 地址::1而不是127.0.0.1了。

参考链接

Category: Linux | Tags: linux 网络 iptables

| Theme: Aeros 2.0 by TheBuckmaker.com