You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

199 lines
15 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 不定期加餐(一) | 八仙过海各显神通透传真实源IP的各种方法
你好我是胜辉。这节加餐课我们来聊聊透传真实源IP的各种方法。
在互联网世界里真实源IP作为一个比较关键的信息在很多场合里都会被服务端程序使用到。比如以下这几个场景
* **安全控制**服务端程序根据源IP进行验证比如查看其是否在白名单中。使用IP验证再结合TLS层面和应用层面的安全机制就形成了连续几道安全门可以说是越发坚固了。
* **进行日志记录**记下这个事务是从哪个源IP发起的方便后期的问题排查和分析乃至进行用户行为的大数据分析。比如根据源IP所在城市的用户的消费特点制定针对性的商业策略。
* **进行客户个性化展现**根据源IP的地理位置的不同展现出不同的页面。以eBay为例如果判断到访问的源IP来自中国那就给你展现一个海淘页面而且还会根据中国客户的特点贴心地给你推荐流行爆款。
虽然源IP信息有这么多用处但是现实情况中这个源IP信息还不是那么好拿。这个原因有很多最主要的还是跟负载均衡LB的设计有关系。
一般来说用户发起HTTP请求到网站VIPVIP所在的LB会把请求转发给后端一前一后分别有两个TCP连接。
* 前一个TCP连接的客户端IP是CIP服务端IP是VIP。
* 后一个TCP连接的客户端IP是LB的SNAT IP服务端IP是SIP。
由此,我们可以得到以下示意图:
![](https://static001.geekbang.org/resource/image/9b/51/9b6101727ca01dbdb2c16b67c1440251.jpg?wh=2000x722)
在这个过程中LB把这两个表面上没有任何联系的TCP连接“映射”了起来所以也只有LB知道从哪个真实源IP这里的CIP来的请求被转发到了哪一个后端的连接上去了。
在这种设计之下可怜的服务端SIP却**只能看到LB的SNAT IP对CIP是一无所知**,就导致了上面说的好几个功能一个都用不上。
不过别急,我们有这么几种方法来解决这个难题。我会按网络层级来一一介绍,分别是应用层方法、传输层方法,还有网络层方法。
我们先看应用层方法。
## 应用层方法
在这一层Web协议的制定者们想到了一个巧妙的办法既然HTTP协议比较灵活那就可以**设计一个新的header用来传递真实源IP它就是X-Forwarded-For**。这个标准最初是Squid的开发工程师提出的很快受到了业界的支持各种web服务器都早已支持了这个header。
> 补充Squid是应用最为广泛的代理和缓存软件之一。
X-Forwarded-For的形式跟其他HTTP header一样也是key: value的形式。key是X-Forwarded-For这个字符串value是一个IP或者用逗号分隔开的多个IP也就是下面这样
```bash
X-Forwarded-For: ip1,ip2,ip3
```
那为什么会有多个IP的情形呢因为一个HTTP请求可能会被多个HTTP代理等系统转发每一级代理都可能会把上一个代理的IP附加到这个X-Forwarded-For头部的值里面。最左边的IP就是真实源IP后面跟着的多个IP就是依次经过的各个代理或者LB的IP。
我们来看个例子。下面是截取的某个抓包文件的HTTP请求的部分能看到X-Forwarded-For头部它的值为真实源IP。同时也看到还有另外一个头部X-Forwarded-Proto它的值为真实客户端跟这个代理之间通信的协议此处为HTTP当然也可以是HTTPS。
![图片](https://static001.geekbang.org/resource/image/dc/8e/dc6f69f0f2dc03be38ec5f6135a8c58e.png?wh=754x76)
不过X-Forwarded-For这个标准虽然用一种相对低的成本解决了“服务器不能获取真实源IP”的问题但它本身还是有一些不足的我们来看一下。
* **源IP信息的伪造问题**
这也是它最大的问题因为这个头部本身没有任何安全保障机制攻击者完全可以任意构造X-Forwarded-For信息来欺骗服务端。
比如如果攻击者知道服务端对某个IP段来的请求进行特殊处理比如会提供更大力度的优惠券那么攻击者就可以在发送请求时候构造一个X-Forwarded-For头部它的值就是这个段内的某个IP。
当服务端收到请求时认为X-Forwarded-For里排在最左边的IP是真实IP而事实上这个是伪造出来的所以可想而知这个请求就可以获取它原本不应该得到的特权了。
* **重复的X-Forwarded-For头部**
HTTP协议本身并不严格要求header是唯一的所以有些情况下HTTP请求可能会携带两个或者更多的X-Forwarded-For头部。
造成这个现象的原因是某些代理或者LB并不是严格按照协议规定的把IP附加到已有的X-Forwarded-For头部而是自己另起一个X-Forwarded-For头部那么这样就导致了重复的X-Forwarded-For。
对于服务端来说在收到这种请求的时候可能会导致信息识别上的错乱。比如某些服务端的逻辑是读取第一个X-Forwarded-For而另外一些服务端程序可能是读取最后一个并无定法。
* **不能解决HTTP和邮件协议以外的真实源IP获取的需求**
X-Forwarded-For解决了HTTP的透传真实源IP的需求但是事实上很多应用并不是基于HTTP协议工作的比如数据库、FTP、syslog等等这些场景也需要“获取真实源IP”这个功能。但是前面说的**X-Forwarded-For只能为HTTP/邮件协议所用**那其他这么多协议和应用难道就成了没妈的孩子永远不能获取到真实源IP了吗
这时候,传输层的方法就上场了。
## 传输层方法
在传输层这一层有不止一种办法可以实现真实源IP透传让我来逐一介绍。
### TOA和TCP Options
TOA全称是TCP Option Address它是**利用TCP Options的字段来承载真实源IP信息**这个是目前比较常见的第四层方案。不过这并非是TCP标准所支持的所以需要通信双方都进行改造。也就是
* 对于发送方来说需要有能力把真实源IP插入到TCP Options里面。
* 对于接收方来说需要有能力把TCP Options里面的IP地址读取出来。
这里我们先来看一下TCP Options在TCP header里面的位置
![图片](https://static001.geekbang.org/resource/image/13/c9/134aa8498fda4d2a212eb58a7705a7c9.png?wh=1259x502)
> [图片来源](https://en.wikipedia.org/wiki/Transmission_Control_Protocol)
可见TCP Options是可变长的最长为40字节第一列的偏移量20到60字节之差。每个Option项由三部分组成
* op-kind
* op-length
* op-data。
TOA采用的kind是254长度为6个字节用于IPv4。我们来看一下TOA的工作原理示意图
![](https://static001.geekbang.org/resource/image/b2/d6/b216a4941885e549ce69f07b932d93d6.jpg?wh=2000x671)
我们可以到Github上[TOA的repo](https://xn--19g)了解到更多的实现细节。比如我们可以看一下TOA源码中toa\_data的数据结构
![图片](https://static001.geekbang.org/resource/image/73/73/73fa81424e7b50412225eef89334b273.png?wh=346x218)
可见opcodeop-kind是一个字节opsizeop-length是1个字节端口客户端的是2个字节ip地址是4个字节也就是TOA传递了真实源IP和真实源端口的信息。
TOA具体的工作原理是TOA模块hook了内核网络中的结构体inet\_stream\_ops的inet\_getname函数替换成了自定义函数。这个自定义函数会判断TCP header中是否有op-kind为254的部分。如果有就取出其中的IP和端口值作为返回值。
这样的话当来自用户空间的程序调用getpeername()这个系统调用时拿到的IP就不再是IP报文的源IP而是TCP Options里面携带的真实源IP了。比如服务器加载TOA后当然LB也要支持TOA那么在access log里面的remote IP一列就会是真实源IP而不加载TOA模块的话就只是LB的SNAT IP了。
### Proxy Protocol
这个方案是HAProxy另外一个广泛应用的反向代理软件工程师提出的。它的实现原理是这样的
* 客户端在TCP握手完成之后在应用层数据发送之前插入一个包这个包的payload就是真实源IP。也就是说在三次握手后第四个包不是应用层请求而是一个包含了真实源IP信息的TCP包这样应用层请求会延后一个包从第五个包开始。
* 服务端也需要支持Proxy Protocol以此来识别三次握手后的这个额外的数据包提取出真实源IP。
我们可以看一下它具体的工作原理:
![](https://static001.geekbang.org/resource/image/a8/cf/a87712ec596bb9d0a34a9fb44yy918cf.jpg?wh=2000x1125)
那么目前除了HAProxy以外其实也有不少软件已经支持了Proxy Protocol比如Nginx以及各大公有云的服务比如AWS亚马逊云和GCP谷歌云。我们还是拿鲜活的抓包信息来展示一下。测试环境是client -> HAProxy (enabled with proxy protocol as proxy) -> nginx (enabled with proxy protocol as server)。
![](https://static001.geekbang.org/resource/image/cc/d3/cc09cac24dbbcc8643c140c1034627d3.jpg?wh=2000x345)
首先我们从客户端发起HTTP请求然后在HAProxy上抓包获取信息如下
![图片](https://static001.geekbang.org/resource/image/7c/0a/7c204094338a08564669f0a33f0fb30a.png?wh=1864x1284)
可见整个抓包文件中第9个包也就是服务端连接的第四个包就是那个关键的携带了真实源IP信息的包我们可以直接在Wireshark下方的报文详情里看到它的文本格式的内容
```bash
PROXY TCP 10.0.2.2 10.0.2.15 51866 80
```
其中10.0.2.2就是真实源IP10.0.2.15是VIP51866是真实源端口80是VIP端口。
而这里你要知道默认的HAProxy和Nginx配置都是不启用Proxy Protocol的所以需要额外进行这些配置。
另外如果中间LB这个例子里是HAProxy启用了Proxy Protocol而后端服务器这个例子里是Nginx没启用那么客户端会收到HTTP 400 bad request。究其原因是因为不启用Proxy Protocol的Nginx会认为握手后的第一个包并没有遵循HTTP协议规范所以给出了HTTP 400的报错回复。
![](https://static001.geekbang.org/resource/image/23/76/237bfc77513710f7b7e06cec5bff3876.jpg?wh=2000x374)
### NetScaler的TCP IP header
这是Citrix也就是NetScaler的厂商提供的自家的方案。它的原理跟Proxy Protocol是类似的也是在握手之后立即发送一个包含真实源IP信息的TCP包而差别仅仅在于**数据格式不同**。也就是说这个方式的原理也可以借用Proxy Protocol的那张图来说明
![](https://static001.geekbang.org/resource/image/0f/11/0f11c82b347902868af12312d737e811.jpg?wh=2000x1125)
然后后端服务器也需要进行适当改造以支持这个行为也就是需要读取相应字段提取出源IP信息。
我们可以来看一下[Citrix官网文档](https://support.citrix.com/article/CTX205670)中的例子:
![图片](https://static001.geekbang.org/resource/image/eb/9c/eb64f4043cc1f8998dc51cabf289749c.png?wh=491x103)
可见在握手的三个包之后第四个包里面包含了真实源IP信息。也就是图中黄色高亮的部分0a 67 06 1e。换算成十进制就是10.103.6.30。
这种算是私有协议了支持场景会比Proxy Protocol更少一些所以需要服务端开发人员对此进行代码改造来让应用程序能够识别这个包里面的信息。
## 网络层方法
不过既然事关IP信息的传递怎么IP层自己反而没有办法呢事实上在这一层确实也有办法比如利用IPIP这样的隧道技术。简单来说就是**用“三角模式”来实现直接的源IP信息的透传**。但它的实现原理,跟前面介绍的几个就有比较明显的区别了。
* 传输层和应用层把真实源IP当做header的一部分传输到后端。
* 网络层直接把真实源IP传输到后端。
让我们看一下三角模式示意图:
![](https://static001.geekbang.org/resource/image/80/4c/80b810923431ac3c307e1b6392c8874c.jpg?wh=1967x827)
具体的IPIP隧道加三角模式的配置细节网上很容易搜到这里就不赘述了。显而易见这种模式里客户端地址CIP是被服务端直接可见的看起来貌似最为直接也不需要任何应用层和传输层的改造。
不过,这种方式的缺点也比较明显。
* **配置繁琐,扩展性不佳**IPIP隧道或者其他隧道技术需要在LB和服务端都进行配置VIP也需要在服务端上配置。我们知道步骤越多出错概率就越大在系统架构选型的时候我们要注意控制这些变量的数目使得系统易于维护。
* **LB无法处理回包**因为回包不再经过LB那么对应用回复的处理就无从实现了比如对HTTP Response的改写就没办法在LB环节做了。如果需要有这些逻辑那么我们要把这部分逻辑回撤到服务器本身来处理。
> 补充当然如果LB跟后端服务器在同一个二层网络里可以把LB配置为服务器的网关使得HTTP响应报文也经过LB不过这个前提条件相对苛刻。
## 小结
这节课我们主要学习了几种透传真实源IP的方法。其中应用层透传真实源IP的方法是利用X-Forwarded-For这个头部把真实源IP传递给后端服务器。这个场景对HTTP应用有效但是对其他应用就不行了所以还要看另外两大类方法。
那么,针对传输层主要是有三种方法:
* 扩展SYN报文的**TCP Options**让它携带真实源IP信息。这个需要对中间的LB和后端服务器都进行小幅的配置改造。
* 利用**Proxy Protocol**。这是一个逐步被各种反向代理和HTTP Server软件接纳的方案可以在不改动代码或者内核配置的情况下只修改反向代理和HTTP Server软件的配置就能做到。
* 利用**特定厂商的方案**如果你用的也是NetScaler可以利用它的相关特性来实现TCP层面的真实源IP透传。不过这也需要你修改应用代码来读取这个信息。
而在网络层,我们可以用**隧道+DSR模式**的方法让真实源IP直接跟服务端“对话”。这个方案的配置稍多另外LB也可能无法处理返回报文所以你需要评估自己的需求后再决定是否采用这一方案。
最后学完了这节课你也要清楚在实际的工作中其实并没有一个普适于一切场景的获取真实源IP的方案而是**应该根据不同的需求和基础架构特点,来选取最适合自己的那一个**。我想这个原则无论对于获取真实源IP这个场景还是其他任何技术选型都应该是我们遵守的法则。就算是衣服的均码也有人穿着不合身呢。要想展现你的身材恐怕只有量身定做才最为靓丽。当然前提是你知道这些选项的存在。
## 思考题
今天的加餐就到这里最后也给你留一道思考题假设你的应用是一个自己开发的基于TCP的应用部署在LB后面那你会选择用上面介绍的那种方法来透传真实源IP信息呢
欢迎在留言区分享你的答案,也欢迎你把今天的内容分享给更多的朋友。