惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
SecWiki News
SecWiki News
P
Proofpoint News Feed
P
Palo Alto Networks Blog
L
LINUX DO - 热门话题
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Spread Privacy
Spread Privacy
A
Arctic Wolf
Recorded Future
Recorded Future
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园_首页
J
Java Code Geeks
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
DataBreaches.Net
量子位
K
Kaspersky official blog
I
InfoQ
W
WeLiveSecurity
Engineering at Meta
Engineering at Meta
Scott Helme
Scott Helme
U
Unit 42
Cyberwarzone
Cyberwarzone
L
Lohrmann on Cybersecurity
Cisco Talos Blog
Cisco Talos Blog
T
Troy Hunt's Blog
Stack Overflow Blog
Stack Overflow Blog
WordPress大学
WordPress大学
Jina AI
Jina AI
Recent Announcements
Recent Announcements
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
C
Cybersecurity and Infrastructure Security Agency CISA
H
Heimdal Security Blog
aimingoo的专栏
aimingoo的专栏
H
Help Net Security
Security Latest
Security Latest
B
Blog RSS Feed
Project Zero
Project Zero
C
Cisco Blogs
S
Securelist
V
Visual Studio Blog
Hugging Face - Blog
Hugging Face - Blog
G
GRAHAM CLULEY
N
Netflix TechBlog - Medium
T
Tor Project blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
GbyAI
GbyAI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
F
Fortinet All Blogs

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Learning Pingora 05 - Connect with TLS | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 02: Astro❤️Password | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog 对博客与笔记的思考 | Yesterday17's Blog Project Anni 之旅(3)自动化 Flutter 应用 CI/CD 上架流程 | Yesterday17's Blog AsobiStage 直接播放链接 | Yesterday17 如何在后分P时代进行投稿——sswa使用详解 | Yesterday17's Blog JSON RPC 与 LSP 协议基础 | Yesterday17's Blog Grajapa Shueisha / BookEnd 加密方式调查 | Yesterday17's Blog 【2022篇+WriteUp】如何再收一个新年红包? | Yesterday17's Blog 如何将良心云的良心功能清理干净 | Yesterday17's Blog 【油猴脚本】bilibili 投稿页面返回旧版+旧版页面强制允许分P上传 | Yesterday17's Blog Cloudr1v1 授权方式分析 | Yesterday17 Typora 1.0.2 逆向实录 | Yesterday17's Blog Project Anni 之旅(2)ValueAfterTable——toml-rs的实现与限制 | Yesterday17's Blog IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记 | Yesterday17's Blog Chaos; Child 汉化补丁 神秘编码探索 | Yesterday17's Blog 镣铐与舞蹈——个性与共性之迷思 | Yesterday17's Blog Go 学习笔记 02 - 找准 io 之道 | Yesterday17's Blog NAT Slipstreaming v1 原理浅析 | Yesterday17's Blog 绕过「9-nine-」的 CDKEY 验证——KrkrPlugin 正(?)向实录 | Yesterday17's Blog 静流的青春纪念册——「サクラノ刻 -櫻の森の下を歩む-」体验版感言 | Yesterday17's Blog Project Anni 之旅 01 - 从 clap-builder 到 derive | Yesterday17's Blog [Google CTF 2021] CPP WriteUp | Yesterday17's Blog 获取 アソビステージ 的实际播放链接 | Yesterday17's Blog 90 行 Rust 代码实现 AsyncTeeReader | Yesterday17's Blog 或许还算有价值一读的文章列表 | Yesterday17's Blog 从零开始的 Seedbox 之旅 | Yesterday17's Blog [随笔]技术型博客行文迷思(1) | Yesterday17's Blog 浅谈 git fetch 的工作方式 | Yesterday17's Blog 『ソーサレス*アライヴ! ~the World's End Fallen Star~』通关感想" | Yesterday17's Blog Rust std::fmt 格式语法简述 | Yesterday17's Blog 日亚修改居住国的解决方案 | Yesterday17's Blog [Windows/Linux] GC553 的 Switch 完美采集之路 | Yesterday17's Blog 【翻译】Subtyping and Variance / 子类型与变型 | Yesterday17's Blog Berd's Red Envelope 2021 WriteUp | Yesterday17's Blog 【中英对照】ALSA 音频 API 使用教程/A Tutorial on Using the ALSA Audio API | Yesterday17's Blog 从 cue_scanner.l 看 CUE Sheet 的词法单元 | Yesterday17's Blog Postman 历史记录导出的解决方案 | Yesterday17's Blog 《恋爱绮谭 不存在的夏天》通关感想 | Yesterday17's Blog [微机实验/TD-PITE] 微机接口综合实验 | Yesterday17's Blog [微机实验/TD-PITE] 键盘扫描及数码管显示实验 | Yesterday17's Blog [微机实验/TD-PITE] 数码管显示实验 | Yesterday17's Blog Airsonic Advanced+Google Drive+Caddy 部署纪实 | Yesterday17's Blog X-NUCA 2020 - hellowasm 题解 | Yesterday17's Blog [微机实验/TD-PITE] 8251 串行接口实验 | Yesterday17's Blog Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog EP.01 「夜の向日葵」 | Yesterday17's Blog [微机实验/TD-PITE] 8254 定时/计数器实验+选做实验 | Yesterday17's Blog [JLU CTF/2020] babywasm WriteUp | Yesterday17's Blog PHP 反序列化与经典利用 | Yesterday17's Blog WebAssembly 逆向简述 | Yesterday17's Blog 『彼女、お借りします』一期完结点评 | Yesterday17's Blog [微机实验/TD-PITE] D/A 转换实验+选做实验 | Yesterday17's Blog [微机实验/TD-PITE] A/D 转换实验+选做实验 | Yesterday17's Blog 开源项目申请 JetBrains Open Source License 简单流程 | Yesterday17's Blog 微软拼音与 JetBrains 搜索快捷键冲突的解决方案 | Yesterday17's Blog [微机实验/TD-PITE] 8259 中断优先级实验+选做实验 | Yesterday17's Blog IFTTT 测试(续) | Yesterday17 IFTTT 测试 | Yesterday17's Blog [微机实验/TD-PITE] 存储器扩展实验+选做实验 | Yesterday17's Blog 新版 GCC 针对 -fdump-translation-unit 的替代方案 | Yesterday17's Blog 一次 HSTS 策略配置的排错之旅 | Yesterday17's Blog YukiNative 踩坑记——Windows 的消息队列 | Yesterday17's Blog 我是我自己——论获取 HTTPS 证书时的验证步骤 | Yesterday17's Blog 【设计文档】对 PUG 的大规模设计修订(1.1) | Yesterday17's Blog GS65 折腾记(2)加装固态,分区,Grub2 引导 Manjaro LiveCD | Yesterday17's Blog 「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录 | Yesterday17's Blog GSuite 探索篇(1)使用 Service Account 向 Google Drive 传输文件 | Yesterday17's Blog 『サクラノ詩 -櫻の森の上を舞う-』通关感想 | Yesterday17's Blog 《ATRI -My Dear Moments-》通关感想 | Yesterday17's Blog [工具][VSCode 扩展] AegiKit——方便 Aegisub 使用的工具箱 | Yesterday17's Blog 贝塞尔曲线、字体矢量化与曲线运算 | Yesterday17's Blog
TProxy 探秘 | Yesterday17's Blog
Yesterday17 · 2020-05-19 · via Yesterday17's Blog

今天趁着群友尝试部署透明代理的机会,彻底学习了一遍 TProxy 的原理。网上原理这方面的内容比较少,可能是因为太过简单了吧,但对我来说还是太神必了果然还是我太菜了

ToC

  • 何为 TProxy?
  • TProxy 原理
    • 入口
    • tproxy_tg4_check
    • tproxy_tg4_v1
    • tproxy_tg4
      • UDP 头部?
      • tproxy_laddr4
      • tproxy_handle_time_wait4
      • tproxy_sk_is_transparent
      • nf_tproxy_assign_sock
      • nf_tproxy_get_sock_v4
    • 目标地址/端口的来源
  • 经典的策略路由
  • TProxy 网关透明代理
  • 结语
  • 参考链接

何为 TProxy?

TProxy 对于使用过透明代理的人来说应该相对比较熟悉了吧,至少名字还是听到过的就比如我,只是知道个名字。

所有的 TProxy 介绍文章都会告诉我们,TProxy 不会修改报文头,但却可以将报文转发。正如官方文档所言:

It redirects the packet to a local socket without changing the packet header in any way.

它会将数据包重定向到一个本地的 socket 上,而不会以任何形式修改包的头部。

那它究竟是怎么工作的呢?

TProxy 原理

出于方便偷懒,这里我们只介绍 IPv4 的情况,IPv6 基本是同理的,这里就不重复了。

这里我们使用的是 Linux 内核版本 4.16.1/net/netfilter/xt_TPROXY.c [2]。

入口

首先我们来看入口:

static struct xt_target tproxy_tg_reg[] __read_mostly = {

{

.name = "TPROXY",

.family = NFPROTO_IPV4,

.table = "mangle",

.target = tproxy_tg4_v0,

.revision = 0,

.targetsize = sizeof(struct xt_tproxy_target_info),

.checkentry = tproxy_tg4_check,

.hooks = 1 << NF_INET_PRE_ROUTING,

.me = THIS_MODULE,

},

{

.name = "TPROXY",

.family = NFPROTO_IPV4,

.table = "mangle",

.target = tproxy_tg4_v1,

.revision = 1,

.targetsize = sizeof(struct xt_tproxy_target_info_v1),

.checkentry = tproxy_tg4_check,

.hooks = 1 << NF_INET_PRE_ROUTING,

.me = THIS_MODULE,

},

#ifdef XT_TPROXY_HAVE_IPV6

{

.name = "TPROXY",

.family = NFPROTO_IPV6,

.table = "mangle",

.target = tproxy_tg6_v1,

.revision = 1,

.targetsize = sizeof(struct xt_tproxy_target_info_v1),

.checkentry = tproxy_tg6_check,

.hooks = 1 << NF_INET_PRE_ROUTING,

.me = THIS_MODULE,

},

#endif

};

static int __init tproxy_tg_init(void)

{

return xt_register_targets(tproxy_tg_reg, ARRAY_SIZE(tproxy_tg_reg));

}

static void __exit tproxy_tg_exit(void)

{

xt_unregister_targets(tproxy_tg_reg, ARRAY_SIZE(tproxy_tg_reg));

}

module_init(tproxy_tg_init);

module_exit(tproxy_tg_exit);

MODULE_LICENSE("GPL");

MODULE_AUTHOR("Balazs Scheidler, Krisztian Kovacs");

MODULE_DESCRIPTION("Netfilter transparent proxy (TPROXY) target module.");

MODULE_ALIAS("ipt_TPROXY");

MODULE_ALIAS("ip6t_TPROXY");

从这里我们可以了解到,TProxy 模块工作在 mangle 表上,且工作(配置成功)前需要经过 tproxy_tg4_check 的检查;成功配置后,则通过 tproxy_tg4_v0tproxy_tg4_v1 运作,这里我们自然是看 v1。不会有人问我为什么吧,追求最新版不是程序员的浪漫吗(

tproxy_tg4_check

在正式开始之前摸个简单的(

static int tproxy_tg4_check(const struct xt_tgchk_param *par)

{

const struct ipt_ip *i = par->entryinfo;

int err;

err = nf_defrag_ipv4_enable(par->net);

if (err)

return err;

if ((i->proto == IPPROTO_TCP || i->proto == IPPROTO_UDP)

&& !(i->invflags & IPT_INV_PROTO))

return 0;

pr_info_ratelimited("Can be used only with -p tcp or -p udp\n");

return -EINVAL;

}

我们看到,想要使用 TProxy 有两个前提条件:

  1. 启用了 nf_defrag_ipv4
  2. 协议为 TCPUDP

tproxy_tg4_v1

static unsigned int

tproxy_tg4_v1(struct sk_buff *skb, const struct xt_action_param *par)

{

const struct xt_tproxy_target_info_v1 *tgi = par->targinfo;

return tproxy_tg4(xt_net(par), skb, tgi->laddr.ip, tgi->lport,

tgi->mark_mask, tgi->mark_value);

}

可以看到,这里我们本质执行的是 tproxy_tg4,传递的参数有:

  • xt_net(par):即 net
  • skb:即 socket_buffer,是这一步中需要处理的本体。
  • laddr.iplport:配置中设置的 ipport,对应的是 --on-ip--on-port
  • mask_maskmask_value:对应 --tproxy-markvalue/mask

tproxy_tg4

static unsigned int

tproxy_tg4(struct net *net, struct sk_buff *skb, __be32 laddr, __be16 lport,

u_int32_t mark_mask, u_int32_t mark_value)

{

const struct iphdr *iph = ip_hdr(skb);

struct udphdr _hdr, *hp;

struct sock *sk;

hp = skb_header_pointer(skb, ip_hdrlen(skb), sizeof(_hdr), &_hdr);

if (hp == NULL)

return NF_DROP;

/* check if there's an ongoing connection on the packet

* addresses, this happens if the redirect already happened

* and the current packet belongs to an already established

* connection */

sk = nf_tproxy_get_sock_v4(net, skb, hp, iph->protocol,

iph->saddr, iph->daddr,

hp->source, hp->dest,

skb->dev, NFT_LOOKUP_ESTABLISHED);

laddr = tproxy_laddr4(skb, laddr, iph->daddr);

if (!lport)

lport = hp->dest;

/* UDP has no TCP_TIME_WAIT state, so we never enter here */

if (sk && sk->sk_state == TCP_TIME_WAIT)

/* reopening a TIME_WAIT connection needs special handling */

sk = tproxy_handle_time_wait4(net, skb, laddr, lport, sk);

else if (!sk)

/* no, there's no established connection, check if

* there's a listener on the redirected addr/port */

sk = nf_tproxy_get_sock_v4(net, skb, hp, iph->protocol,

iph->saddr, laddr,

hp->source, lport,

skb->dev, NFT_LOOKUP_LISTENER);

/* NOTE: assign_sock consumes our sk reference */

if (sk && tproxy_sk_is_transparent(sk)) {

/* This should be in a separate target, but we don't do multiple

targets on the same rule yet */

skb->mark = (skb->mark & ~mark_mask) ^ mark_value;

pr_debug("redirecting: proto %hhu %pI4:%hu -> %pI4:%hu, mark: %x\n",

iph->protocol, &iph->daddr, ntohs(hp->dest),

&laddr, ntohs(lport), skb->mark);

nf_tproxy_assign_sock(skb, sk);

return NF_ACCEPT;

}

pr_debug("no socket, dropping: proto %hhu %pI4:%hu -> %pI4:%hu, mark: %x\n",

iph->protocol, &iph->saddr, ntohs(hp->source),

&iph->daddr, ntohs(hp->dest), skb->mark);

return NF_DROP;

}

从头看起。307 行定义并获取了 IP 报头;308 行定义了 UDP 报头,并且在 311 行获取了这个报头的内容(指针);309 行定义了 socket 指针。这几步是初始的定义过程。

UDP 头部?

这里产生了一个疑问:为什么第 309 行是 udphdr 呢?实际上,通过观察,我们会发现:TCP 报头和 UDP 报头的前 4 个字节的意义是一致的,均代表源端口目标端口

TCP 头部
TCP 头部
UDP 头部
UDP 头部

而由于后续的需要,我们要用到这两个端口,因此在这里我们尝试获取 UDP 报文的头部。于是 312-313 行:当获取出现错误时丢弃报文的行为意图也就非常清楚了。

下面我们跳着看,先看 324-326 行。这三行调用了 tproxy_laddr4 函数。


tproxy_laddr4

static inline __be32

tproxy_laddr4(struct sk_buff *skb, __be32 user_laddr, __be32 daddr)

{

struct in_device *indev;

__be32 laddr;

if (user_laddr)

return user_laddr;

laddr = 0;

indev = __in_dev_get_rcu(skb->dev);

for_primary_ifa(indev) {

laddr = ifa->ifa_local;

break;

} endfor_ifa(indev);

return laddr ? laddr : daddr;

}

这个函数是用来获取 laddr 的。函数逻辑非常明确:当存在 user_laddr,从配置上来说就是用户定义了 --to-ip 时,使用用户定义的 ip;否则就尝试获取本地 IP(即 ifa_local);当未获取到时最后 fallback 到原本的目标地址。


在获得了 laddr 之后,我们还需要获得一个端口。因此,当未设置端口时,我们直接使用了从报文头部获得的目标端口。

回到 319-322 行。这里,包括下面的 329-331 行,处理的是当前已经建立但却没有遵循 TProxy 配置的连接。这种情况最常见的出现情形是在这条规则刚被添加之后,此时需要处理现有的连接。这里我们先跳过 nf_tproxy_get_sock_v4,只要知道它是用来获取 sk 的就够了。

如果我们得到了 sk,那我们需要处理的首先就是 TCPTIME_WAIT 状态。对于 TCP TIME_WAIT 的解释可以参考[3]。由于在这种状态下,客户端可能会通过 SYN 与服务端重新建立连接,因此我们应该尝试把连接交给 TProxy 处理。我们来看看 TProxy 是怎么处理 TIME_WAIT 的:


tproxy_handle_time_wait4

/**

* tproxy_handle_time_wait4 - handle IPv4 TCP TIME_WAIT reopen redirections

* @skb: The skb being processed.

* @laddr: IPv4 address to redirect to or zero.

* @lport: TCP port to redirect to or zero.

* @sk: The TIME_WAIT TCP socket found by the lookup.

*

* We have to handle SYN packets arriving to TIME_WAIT sockets

* differently: instead of reopening the connection we should rather

* redirect the new connection to the proxy if there's a listener

* socket present.

*

* tproxy_handle_time_wait4() consumes the socket reference passed in.

*

* Returns the listener socket if there's one, the TIME_WAIT socket if

* no such listener is found, or NULL if the TCP header is incomplete.

*/

static struct sock *

tproxy_handle_time_wait4(struct net *net, struct sk_buff *skb,

__be32 laddr, __be16 lport, struct sock *sk)

{

const struct iphdr *iph = ip_hdr(skb);

struct tcphdr _hdr, *hp;

hp = skb_header_pointer(skb, ip_hdrlen(skb), sizeof(_hdr), &_hdr);

if (hp == NULL) {

inet_twsk_put(inet_twsk(sk));

return NULL;

}

if (hp->syn && !hp->rst && !hp->ack && !hp->fin) {

/* SYN to a TIME_WAIT socket, we'd rather redirect it

* to a listener socket if there's one */

struct sock *sk2;

sk2 = nf_tproxy_get_sock_v4(net, skb, hp, iph->protocol,

iph->saddr, laddr ? laddr : iph->daddr,

hp->source, lport ? lport : hp->dest,

skb->dev, NFT_LOOKUP_LISTENER);

if (sk2) {

inet_twsk_deschedule_put(inet_twsk(sk));

sk = sk2;

}

}

return sk;

}

顶部的注释清晰地说明了函数的用途。来看代码,267-274 行获取了 IP 头部和 TCP 头部,而 276-289 行则对 SYN 进行了特殊处理:它尝试去获得了当前监听目标 IP 和目标端口的 socket,当存在时,则将 sk 替换。


处理完 TCP_TIME_WAIT,接下来就是不存在 sk 的情况了。如 332-338 行所写,这种情况下的连接只需要尝试获得是否有监听目标 IP:端口的存在就可以了。

到这里为止,所有尝试获取 sk 的行为都已经结束了。也就是说,接下来就是魔法的时刻了。我们来看 341-351 行:当 sk 存在且 tproxy_sk_is_transparent 判断成功时,我们设置当前 skbmark,并且通过 nf_tproxy_assign_sock 设置 skbsk,最后返回 ACCEPT

至此,TProxy 的处理结束。

tproxy_sk_is_transparent

这里我们来看一下上面为了一气呵成说到处理结束为止所以没有讲的两个函数之一:

static bool tproxy_sk_is_transparent(struct sock *sk)

{

switch (sk->sk_state) {

case TCP_TIME_WAIT:

if (inet_twsk(sk)->tw_transparent)

return true;

break;

case TCP_NEW_SYN_RECV:

if (inet_rsk(inet_reqsk(sk))->no_srccheck)

return true;

break;

default:

if (inet_sk(sk)->transparent)

return true;

}

sock_gen_put(sk);

return false;

}

这里限定了需要处理的 sk 状态类型:

  • TCP_TIME_WAIT 状态下 启用了tw_transparent 的(这里的 tw 其实就是 time wait 的缩写。
  • TCP_NEW_SYN_RECV 状态下启用了 no_srccheck 的。
  • transparent 的。

其实,这里检查的是 sk 是否支持透明代理。正如官方文档所描述:

Because of certain restrictions in the IPv4 routing output code you’ll have to modify your application to allow it to send datagrams from non-local IP addresses. All you have to do is enable the (SOL_IP, IP_TRANSPARENT) socket option before calling bind:

由于 IPv4 路由输出代码的限制(译注:其实 IPv6 也有),你必须修改程序使其能够从非本地的 IP 地址发送数据报。你要做的就是在 bind 之前启用 (SOL_IP, IP_TRANSPARENT)socket 选项。

nf_tproxy_assign_sock

/* assign a socket to the skb -- consumes sk */

static void

nf_tproxy_assign_sock(struct sk_buff *skb, struct sock *sk)

{

skb_orphan(skb);

skb->sk = sk;

skb->destructor = sock_edemux;

}

这里总共做了三件事情:

  1. 将原本 skbsk 销毁。
  2. 将新的 sk 赋给 skb
  3. skbdestructor 设置为 sock_edemux

nf_tproxy_get_sock_v4

/*

* This is used when the user wants to intercept a connection matching

* an explicit iptables rule. In this case the sockets are assumed

* matching in preference order:

*

* - match: if there's a fully established connection matching the

* _packet_ tuple, it is returned, assuming the redirection

* already took place and we process a packet belonging to an

* established connection

*

* - match: if there's a listening socket matching the redirection

* (e.g. on-port & on-ip of the connection), it is returned,

* regardless if it was bound to 0.0.0.0 or an explicit

* address. The reasoning is that if there's an explicit rule, it

* does not really matter if the listener is bound to an interface

* or to 0. The user already stated that he wants redirection

* (since he added the rule).

*

* Please note that there's an overlap between what a TPROXY target

* and a socket match will match. Normally if you have both rules the

* "socket" match will be the first one, effectively all packets

* belonging to established connections going through that one.

*/

static inline struct sock *

nf_tproxy_get_sock_v4(struct net *net, struct sk_buff *skb, void *hp,

const u8 protocol,

const __be32 saddr, const __be32 daddr,

const __be16 sport, const __be16 dport,

const struct net_device *in,

const enum nf_tproxy_lookup_t lookup_type)

{

struct sock *sk;

struct tcphdr *tcph;

switch (protocol) {

case IPPROTO_TCP:

switch (lookup_type) {

case NFT_LOOKUP_LISTENER:

tcph = hp;

sk = inet_lookup_listener(net, &tcp_hashinfo, skb,

ip_hdrlen(skb) +

__tcp_hdrlen(tcph),

saddr, sport,

daddr, dport,

in->ifindex, 0);

if (sk && !refcount_inc_not_zero(&sk->sk_refcnt))

sk = NULL;

/* NOTE: we return listeners even if bound to

* 0.0.0.0, those are filtered out in

* xt_socket, since xt_TPROXY needs 0 bound

* listeners too

*/

break;

case NFT_LOOKUP_ESTABLISHED:

sk = inet_lookup_established(net, &tcp_hashinfo,

saddr, sport, daddr, dport,

in->ifindex);

break;

default:

BUG();

}

break;

case IPPROTO_UDP:

sk = udp4_lib_lookup(net, saddr, sport, daddr, dport,

in->ifindex);

if (sk) {

int connected = (sk->sk_state == TCP_ESTABLISHED);

int wildcard = (inet_sk(sk)->inet_rcv_saddr == 0);

/* NOTE: we return listeners even if bound to

* 0.0.0.0, those are filtered out in

* xt_socket, since xt_TPROXY needs 0 bound

* listeners too

*/

if ((lookup_type == NFT_LOOKUP_ESTABLISHED && (!connected || wildcard)) ||

(lookup_type == NFT_LOOKUP_LISTENER && connected)) {

sock_put(sk);

sk = NULL;

}

}

break;

default:

WARN_ON(1);

sk = NULL;

}

pr_debug("tproxy socket lookup: proto %u %08x:%u -> %08x:%u, lookup type: %d, sock %p\n",

protocol, ntohl(saddr), ntohs(sport), ntohl(daddr), ntohs(dport), lookup_type, sk);

return sk;

}

这就是获得 sk 的函数了。它根据协议的不同又分为 TCPUDP 两类。对于 TCP,没有什么特殊的地方,看函数名基本都能顾名思义,这里略过;对于 UDP,我们同样是首先先 lookup,然后根据 lookup_type 的情况解除对 sk 的引用。这里的 sock_put 就是减少引用数量的,同时当无引用时,会直接 sk_free(sk)

目标地址/端口的来源

读完上面的内容,相信这个问题也就迎刃而解了。正是由于 TProxy 并没有修改报文内容,而只是将报文的 socket 给偷梁换柱了,因此报文经过的也正是正常的 Linux 处理流程。因此,通过系统调用就可以获得原报文的目标地址和目标端口了。

经典的策略路由

要实现透明代理,离不开 iproute 的配合。下面就是最经典的策略路由的实现[5]:

iptables -t mangle -I PREROUTING -p udp --dport 5301 -j MARK --set-mark 1

# 等价

# nft insert rule ip mangle PREROUTING udp dport 5301 counter meta mark set 0x1

ip rule add fwmark 1 lookup 100

ip route add local 0.0.0.0/0 dev lo table 100

我们一行一行看:第一(至三)行添加了一条 iptables 规则,负责将目标端口为 5301UDP 流量打上 mark 为 1;第五行告诉 iproute 对于 fwmark=1 的流量查表 100;而最后一行则声明:所有的 IPv4 地址在表 100 中均为 local

对于 TProxy 来说,道理也是一样的,只不过有一件事情比较特殊:TProxy 的目标地址不是本机。也就是说,如果不设置 mark,那么这份报文就会被转发(被 FORWARD 链处理)而不是被 INPUT 链处理[6]。为了实现透明代理,这份报文必须被本机处理,因此 TProxy 就必须设置 --tproxy-mask。而为了让报文不被转发,这里就需要辅以上面 2-3 行的策略路由了。

完整的 TProxy 实现命令如下:

# 策略路由

ip rule add fwmark 1 lookup 100

ip route add local 0.0.0.0/0 dev lo table 100

# TProxy

iptables -t mangle -I PREROUTING -p udp --dport 53 -j TPROXY --on-port 1090 --tproxy-mark 1

TProxy 网关透明代理

我们知道,网关发出的流量走的是 OUTPUT 链,之后的步骤就没有 PREROUTING 的位置了。那是不是就意味着网关自身就一定无法实现透明代理了呢?答案是否定的。

还是回到刚才的策略路由。现在我们已经有了这样一个将所有 IPv4 地址都当作本机的策略路由,那我们只要让我们的流量经过策略路由选择之后再回到网关,是不是就相当于从 NETWORK 收到一份新的报文呢?

这里我们引用另外一张图[7]:

我们来观察这张图。Local Processing of Data 先经过第一次路由选择,再流经 mangleOUTPUT 链、natOUTPUT 链等等,进行第二次路由选择,最后 POSTROUTING 并发送。我们发现,最后实际上数据报又回到了 Network A,而 Network A 是会把 Packet In 送到 PREROUTING 的。也就是说,只要 OUTPUT 的最后我们的目标恰好是本机的话,那么这份数据报就会重新回到 iptables 的手中,也就和局域网的其他机器所发送的报文一样了。

也就是说,网关发送至网关原地 TP的数据报和局域网发送至网关的数据报其实并没有区别。正因如此,我们可以在 mangle 表的 OUTPUT 链下手设置 fwmark。只要和上文中一样,将 mark 设置成 1,那么这份数据报就会被作为流入流量重新发送给网关自己,于是问题就迎刃而解了。

结语

iptables 是伟大的存在,TProxy 更是给我们提供了无限的可能性。

TProxy 的核心真的很简单,正是因为它的简单,反而让初见的我不知所措,直到读完源码才明白其真意所在。

写这篇文章之前和写下这篇文章时的探索让我对 Linux 的网络有了更深的了解,真的可以说是收获颇丰。这也是我第一次接触 Linux 内核的某一部分,全文如果出现错误的话还敬请各位斧正。

最后留一个很简单的小问题吧:为什么 TProxy 的网关的最大文件描述符数量要设置地较高呢?如果你对 Linux 的网络有一定了解,或者经由这篇文章对 Linux 的网络有了些许了解的话,相信这个问题的答案已经呼之欲出了。

参考链接