from-ss-redir-to-linux-nat
今年4月,在家的時候意外看到了ofollow,noindex">ss-redir 透明代理 ,對其中的詳細說明持有懷疑態度:
由於筆者才疏學淺,剛開始居然以為 TCP 透明代理和 UDP 透明代理是一樣的,只要無腦 REDIRECT 到 ss-redir 監聽埠就可以了。
…
但是,上面這種情況只針對 TCP;對於 UDP,如果你做了 DNAT,就無法再獲取資料包的原目的地址和目的埠了。
於是對此專門做了一番調研。整篇分為三部分:第一部分是我對上述敘述的調研結果,第二部分討論TPROXY,第三部分敘述一些NAT的知識。
ss-redir中的UDP REDIRECT問題
ss-redir的原理很簡單:使用iptables對PREROUTING與OUTPUT的TCP/UDP流量進行REDIRECT(REDIRECT是DNAT的特例),ss—redir在捕獲網路流量後,通過一些技術手段 獲取REDIRECT之前的目的地址(dst)與埠(port),連同網路流量一起轉發至遠端伺服器。
針對TCP連線,的確是因為Linux Kernel連線跟蹤機制的實現才使獲取資料包原本的dst和port成為可能,但這種連線跟蹤機制並非只存在於TCP連線中,UDP連線同樣存在,conntrack -p udp
便能看到UDP的連線跟蹤記錄。核心中有關TCP與UDP的NAT原始碼/net/netfilter/nf_nat_proto_tcp.c
和/net/netfilter/nf_nat_proto_udp.c
幾乎一模一樣,都是根據NAT的型別做SNAT或DNAT。
那這究竟是怎麼一回事?為什麼對於UDP連線就失效了呢?
回過頭來看看ss-redir有關獲取TCP原本的dst和port的原始碼,核心函式是getdestaddr
:
static int getdestaddr(int fd, struct sockaddr_storage *destaddr) { socklen_t socklen = sizeof(*destaddr); int error= 0; error = getsockopt(fd, SOL_IPV6, IP6T_SO_ORIGINAL_DST, destaddr, &socklen); if (error) { // Didn't find a proper way to detect IP version. error = getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, destaddr, &socklen); if (error) { return -1; } } return 0; }
在核心原始碼中搜了下有關SO_ORIGINAL_DST
的東西,看到了getorigdst
:
/* Reversing the socket's dst/src point of view gives us the reply mapping. */ static int getorigdst(struct sock *sk, int optval, void __user *user, int *len) { const struct inet_sock *inet = inet_sk(sk); const struct nf_conntrack_tuple_hash *h; struct nf_conntrack_tuple tuple; memset(&tuple, 0, sizeof(tuple)); lock_sock(sk); tuple.src.u3.ip = inet->inet_rcv_saddr; tuple.src.u.tcp.port = inet->inet_sport; tuple.dst.u3.ip = inet->inet_daddr; tuple.dst.u.tcp.port = inet->inet_dport; tuple.src.l3num = PF_INET; tuple.dst.protonum = sk->sk_protocol; release_sock(sk); /* We only do TCP and SCTP at the moment: is there a better way? */ if (tuple.dst.protonum != IPPROTO_TCP && tuple.dst.protonum != IPPROTO_SCTP) { pr_debug("SO_ORIGINAL_DST: Not a TCP/SCTP socket\n"); return -ENOPROTOOPT; }
We only do TCP and SCTP at the moment 。Oh,shit!只針對TCP與SCTP才能這麼做,並非技術上不可行,只是人為地阻止罷了。
TPROXY
為了在redirect UDP後還能夠獲取原本的dst和port,ss-redir採用了TPROXY。Linux系統有關TPROXY的設定是以下三條命令:
# iptables -t mangle -A PREROUTING -p udp -j TPROXY --tproxy-mark 0x2333/0x2333 --on-ip 127.0.0.1 --on-port 1080 # ip rule add fwmark 0x2333/0x2333 pref 100 table 100 # ip route add local default dev lo table 100
獲取原本的dst和port的相關原始碼如下:
static int get_dstaddr(struct msghdr *msg, struct sockaddr_storage *dstaddr) { struct cmsghdr *cmsg; for (cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) { if (cmsg->cmsg_level == SOL_IP && cmsg->cmsg_type == IP_RECVORIGDSTADDR) { memcpy(dstaddr, CMSG_DATA(cmsg), sizeof(struct sockaddr_in)); dstaddr->ss_family = AF_INET; return 0; } else if (cmsg->cmsg_level == SOL_IPV6 && cmsg->cmsg_type == IPV6_RECVORIGDSTADDR) { memcpy(dstaddr, CMSG_DATA(cmsg), sizeof(struct sockaddr_in6)); dstaddr->ss_family = AF_INET6; return 0; } } return 1; } int create_server_socket(const char *host, const char *port) { ... #ifdef MODULE_REDIR if (setsockopt(server_sock, SOL_IP, IP_TRANSPARENT, &opt, sizeof(opt))) { ERROR("[udp] setsockopt IP_TRANSPARENT"); exit(EXIT_FAILURE); } if (rp->ai_family == AF_INET) { if (setsockopt(server_sock, SOL_IP, IP_RECVORIGDSTADDR, &opt, sizeof(opt))) { FATAL("[udp] setsockopt IP_RECVORIGDSTADDR"); } } else if (rp->ai_family == AF_INET6) { if (setsockopt(server_sock, SOL_IPV6, IPV6_RECVORIGDSTADDR, &opt, sizeof(opt))) { FATAL("[udp] setsockopt IPV6_RECVORIGDSTADDR"); } } #endif ... }
大意就是在mangle表的PREROUTING中為每個UDP資料包打上0x2333/0x2333標誌,之後在路由選擇中將具有0x2333/0x2333標誌的資料包投遞到本地環回裝置上的1080埠;對監聽0.0.0.0地址的1080埠的socket啟用IP_TRANSPARENT
標誌,使IPv4路由能夠將非本機的資料報投遞到傳輸層,傳遞給監聽1080埠的ss-redir。
IP_RECVORIGDSTADDR
與IPV6_RECVORIGDSTADDR
則表示獲取送達資料包的dst與port。
可問題來了:要知道mangle表並不會修改資料包,那麼TPROXY是如何做到在不修改資料包的前提下將非本機dst的資料包投遞到換回裝置上的1080埠呢?
與之有關的核心原始碼我沒有完全看懂。根據TProxy - Transparent proxying, again 和2.6.26時代的patch set ,在netfilter中的PREROUTING階段,將符合規則的IP資料報skb_buff中的成員sk(它表示資料包從屬的套接字 )給assign_sock ,這個sock就是利用iptables TPROXY的target資訊找到的:
// /net/netfilter/xt_TPROXY.c 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) { ... /* 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; } ... }
sock是根據四元組saddr
,sport
,daddr
,dport
來選擇的,其中saddr
與sport
來自skb_buff,另外倆為target所定義。沒搞懂的地方在於:在ip_rcv_finish
中,是怎樣將資料包投遞到上層協議以及指定埠的?
目前的猜測如下:
// kernel version 4.17 - ip_route_input_noref - ip_route_input_rcu - ip_route_input_slow - fib_lookup - fib_table_lookup - res->type = fa->fa_type; - if (res->type == RTN_LOCAL) { ... goto local_input; } -skb_dst_set_noref(skb, &rth->dst); - rth = rt_dst_alloc(l3mdev_master_dev_rcu(dev) ? : net->loopback_dev, flags | RTCF_LOCAL, res->type, IN_DEV_CONF_GET(in_dev, NOPOLICY), false, do_cache); - if (flags & RTCF_LOCAL) rt->dst.input = ip_local_deliver;
通過查詢路由表確定res-type
的型別為RTN_LOCAL
,goto到local_input,進而呼叫rt_dst_alloc
,形參引數(flag & RTCF_LOCAL) == true
,設定了rt->dst.input
是ip_local_deliver
。ip_local_deliver
中使用協議回撥函式handler
來進一步處理資料包。
進入傳輸層後,對IPv4下的UDP協議來說,它的handler
為
udplite_rcv
(v4.17)
,通過呼叫skb_steal_sock
來獲取sock,這個sock與TPROXY中在nf_tproxy_get_sock_v4
獲取到的sock是一致的。sock的判斷是根據compute_score
計算的得分來選擇的,分高者贏。
// UDP .handler = udplite_rcv - udplite_rcv - __udp4_lib_rcv - sk = skb_steal_sock(skb); ... ret = udp_queue_rcv_skb(sk, skb); // TPROXY - nf_tproxy_get_sock_v4 - udp4_lib_lookup - __udp4_lib_lookup - __udp4_lib_lookup_skb - __udp4_lib_lookup - udp4_lib_lookup2 // /net/ipv4/udp.c: get sock static struct sock *udp4_lib_lookup2(struct net *net, __be32 saddr, __be16 sport, __be32 daddr, unsigned int hnum, int dif, int sdif, bool exact_dif, struct udp_hslot *hslot2, struct sk_buff *skb) { struct sock *sk, *result; int score, badness; u32 hash = 0; result = NULL; badness = 0; udp_portaddr_for_each_entry_rcu(sk, &hslot2->head) { score = compute_score(sk, net, saddr, sport, daddr, hnum, dif, sdif, exact_dif); if (score > badness) { if (sk->sk_reuseport) { hash = udp_ehashfn(net, daddr, hnum, saddr, sport); result = reuseport_select_sock(sk, hash, skb, sizeof(struct udphdr)); if (result) return result; } badness = score; result = sk; } } return result; }
有趣的是,在查詢資料過程中,我還看到了這篇文章:TPROXY之殤-NAT裝置加代理的惡果 。
最後來回到原點,談一談NAT。
NAT
根據RFC 2663 ,NAT分為基本網路地址轉換(Basic NAT,also called a one-to-one NAT)和網路地址埠轉換(NAPT(network address and port translation),other names include port address translation (PAT), IP masquerading, NAT overload and many-to-one NAT)兩類。基本網路地址轉換僅支援地址轉換,不支援埠對映,要求每一個內網地址都對應一個公網地址;網路地址埠轉換支援埠的對映,允許多臺主機共享一個公網地址。支援埠轉換的NAT又可以分為兩類:源地址轉換(SNAT)和目的地址轉換(DNAT)。前一種情形下發起連線的計算機的IP地址將會被重寫,使得內網主機發出的資料包能夠到達外網主機。後一種情況下被連線計算機的IP地址將被重寫,使得外網主機發出的資料包能夠到達內網主機。
Linux下,iptables的SNAT除了SNAT target外,還有MASQUERADE、NETMAP。MASQUERADE target與SNAT差不多,區別主要是MASQUERADE能夠自動選擇出口網絡卡的動態IP地址,而NETMAP則是隻轉換IP地址。DNAT的target有DNAT、REDIRECT,區別是REDIRECT只進行埠轉換而IP地址並不會改變。還有一類target叫做NETMAP,只轉換IP地址,同時擁有SNAT與DNAT的功能。討論Linux kernelNAT實現的文章不少,比如iptables深入解析-nat篇 ,在此不想談論這些,而是其他的一些東西。
時隔一個半月,繼續…
- REDIRECT只進行埠對映,並不改變IP地址。這點可以在原始碼 中看到
// /net/netfilter/nf_nat_redirect.c, function: nf_nat_redirect_ipv4 /* Local packets: make them go to loopback */ if (hooknum == NF_INET_LOCAL_OUT) { newdst = htonl(0x7F000001); } else { struct in_device *indev; struct in_ifaddr *ifa; newdst = 0; rcu_read_lock(); indev = __in_dev_get_rcu(skb->dev); if (indev && indev->ifa_list) { ifa = indev->ifa_list; newdst = ifa->ifa_local; } rcu_read_unlock(); if (!newdst) return NF_DROP; }
NAT穿透
NAT穿透 是比較常見的一個問題,在P2P中被廣泛應用。在瞭解NAT穿透之前需要了解NAT的種類,Wikipedia上面給了很詳細的說明 。STUN 是一種網路協議,它允許位於NAT(或多重NAT)後的客戶端找出自己的公網地址,查出自己位於哪種型別的NAT之後以及NAT為某一個本地埠所繫結的Internet端埠。這些資訊被用來在兩個同時處於NAT路由器之後的主機之間建立UDP通訊。該協議由RFC 5389定義。UPnP是由“通用即插即用論壇”推廣的一套網路協議。該協議的目標是使家庭網路(資料共享、通訊和娛樂)和公司網路中的各種裝置能夠相互無縫連線,並簡化相關網路的實現。UPnP通過定義和釋出基於開放、因特網通訊網協議標準的UPnP裝置控制協議來實現這一目標,也是NAT穿透的標準之一。
IPSec中的NAT
提起NAT的源於一篇gist樸素VPN:一個純核心級靜態隧道 ,上面提到的東西在這裡不提。值得注意的是,IPSec本身就有UDP封裝的配置,也有響應的RFC規定了如何穿透NAT,但這裡為什麼要多此一舉呢?(網上炸藕哦
結束與08月01日18點38分,太懶了,不寫了。
參考資料
- ss-redir 透明代理
- [TPROXY] implemented IP_RECVORIGDSTADDR socket option
- TProxy - Transparent proxying, again
- qsorix/udp_socket_addr.cc
- 【Linux 核心網路協議棧原始碼剖析】網路棧主要結構介紹(socket、sock、sk_buff,etc)
- TPROXY之殤-NAT裝置加代理的惡果
- Understanding Network Address Translation, NAT
- Network address translation
- RFC 2663: IP Network Address Translator (NAT) Terminology and Considerations
- iptables深入解析-nat篇
- 從DNAT到netfilter核心子系統,淺談Linux的Full Cone NAT實現
- Linux 核心態實現 Full Cone NAT(2)
- NAT穿透
- STUN
- 樸素VPN:一個純核心級靜態隧道