研究IPv6 socket編程原因:
WWDC2015蘋果宣布在ios9支持純IPv6的網(wǎng)絡(luò)服務(wù),并且要求2016年提交到app store的應(yīng)用必須兼容純IPv6的網(wǎng)絡(luò),要求適配的系統(tǒng)版本是ios9以上(包括ios9)。
寫這篇文章雖然是來源于iOS的需求,但是下面的內(nèi)容除了特別說明外,大部分都適用于其他平臺(tái)。
IPv6的復(fù)雜度之一,在于和IPv4的兼容和相互訪問。本文會(huì)提及其他的互相訪問技術(shù),但是重點(diǎn)是NAT64,也是一般手機(jī)用戶最有可能遇到的純IPv6環(huán)境。
本文重點(diǎn)在不同IP stack組合的處理方式和判斷客戶端支持的IP stack。
為了降低問題的復(fù)雜性,我們先把v4 socket排除掉,統(tǒng)一使用v6 socket。v6 socket的區(qū)別是使用AF_INET6來創(chuàng)建。
IPv6轉(zhuǎn)換機(jī)制有很多種,蘋果期望iOS app能兼容NAT64/DNS64的方式,因此其他方式我們先不考慮。
socket api支持RFC 4038 - Application Aspects of IPv6 Transition
v4 socket接口只能支持IPv4 stack
v6 socket能支持IPv4 stack和IPv6 stack
服務(wù)器IP
返回v4 IP
返回v6 IP
用戶本地IP stack
IPv4-only
IPv6-only
IPv4-IPv6 Dual stack
各種IPv6轉(zhuǎn)換機(jī)制
NAT64/DNS6464:ff9b::/96用于v6的本地網(wǎng)絡(luò)通過NAT訪問v4的資源。RFC 6146、RFC 6147
6to42002::/16用于兩個(gè)擁有v4公網(wǎng)地址的IPv6 only子網(wǎng)的互相訪問。RFC 6343
Teredo tunneling2001::/32通過隧道的方式讓兩個(gè)IPv6 only子網(wǎng)互相訪問,沒有NAT問題。RFC 4380
464XLAT 用于程序只有v4地址(使用v4 socket),但是本地網(wǎng)絡(luò)是ipv6網(wǎng)絡(luò),程序需要訪問v4資源,類似NAT64,不過區(qū)別在于服務(wù)器是運(yùn)營商提供,手機(jī)上需要安裝CLAT服務(wù)。RFC 6877
還有很多兼容方案,復(fù)雜程度都很高,這里不介紹了
v4 ip + IPv4-only or IPv4-IPv6 Dual stack
在這樣的情況下我們雖然用的是v6的socket,但是必須要讓socket走的是v4的協(xié)議。這里,讓我們先了解下IPv6的保留地址(類似IPv4,192.168.., 127...*這種)這里假設(shè)讀者已經(jīng)對(duì)IPv6地址組成和書寫方式有一定了解的了解。
::ffff:0:0/96 — This prefix is designated as an IPv4-mapped IPv6
address. With a few exceptions, this address type allows the transparent
use of the Transport Layer protocols over IPv4 through the IPv6
networking application programming interface. Server applications only
need to open a single listening socket to handle connections from
clients using IPv6 or IPv4 protocols. IPv6 clients will be handled
natively by default, and IPv4 clients appear as IPv6 clients at their
IPv4-mapped IPv6 address. Transmission is handled similarly; established
sockets may be used to transmit IPv4 or IPv6 datagram, based on the
binding to an IPv6 address, or an IPv4-mapped address. (See also
Transition mechanisms.) [^1]
從上文可以看到如果服務(wù)器地址為128.0.0.128,我們轉(zhuǎn)換成IPv4-mapped IPv6 address::ffff:128.0.0.128或者純16進(jìn)制::ffff:ff00:00ff, 然后賦值給sockaddr_in6.sin6_addr = "::ffff:128.0.0.128";(注意這里是偽代碼,真正代碼還要用inet_pton進(jìn)行轉(zhuǎn)換)。這個(gè)socket雖然用了IPv6的sockaddr_in6,但實(shí)際上走的是IPv4 stack。
IPv4-mapped IPv6 address是讓用戶能夠使用一致的socket api,來訪問IPv4和IPv6網(wǎng)絡(luò)。
上文提及RFC 4038 - Application Aspects of IPv6 Transition對(duì)這種情況進(jìn)行說明。
//IPv4-mapped IPv6 address sample
//address init
const char* ipv4mapped_str ="::FFFF:14.17.32.211";
in6_addr ipv4mapped_addr = {0};
int v4mapped_r = inet_pton(AF_INET6, ipv4mapped_str, &ipv4mapped_addr);
sockaddr_in6 v4mapped_addr = {0};
v4mapped_addr.sin6_family = AF_INET6;
v4mapped_addr.sin6_port = htons(80);
v4mapped_addr.sin6_addr = ipv4mapped_addr;
//socket connect
int v4mapped_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
std::string v4mapped_error;
if (0 != connect(v4mapped_sock, (sockaddr*)&v4mapped_addr, 28))
{
v4mapped_error = strerror(errno);
}
//get local ip
sockaddr_in6 v4mapped_local_addr = {0};
socklen_t v4mapped_local_addr_len = 28;
char v4mapped_str_local_addr[64] = {0};
getsockname(v4mapped_sock, (sockaddr*)&v4mapped_local_addr, &v4mapped_local_addr_len);
inet_ntop(v4mapped_local_addr.sin6_family, &v4mapped_local_addr.sin6_addr, v4mapped_str_local_addr, 64);
close(v4mapped_sock);
這里是重點(diǎn),也是蘋果要求支持的主要場(chǎng)景。這里會(huì)涉及到NAT64/DNS64,關(guān)于這個(gè)環(huán)境的搭建請(qǐng)參考Supporting IPv6 DNS64/NAT64 Networks(廢棄了的SIIT技術(shù)我們就不討論了)
這里我們先看看wikipedia對(duì)NAT64/DNS64的描述。
NAT64 is a mechanism to allow IPv6 hosts to communicate with IPv4
servers. The NAT64 server is the endpoint for at least one IPv4 address
and an IPv6 network segment of 32-bits, e.g., 64:ff9b::/96 (RFC 6052,
RFC 6146). The IPv6 client embeds the IPv4 address with which it wishes
to communicate using these bits, and sends its packets to the resulting
address. The NAT64 server then creates a NAT-mapping between the IPv6
and the IPv4 address, allowing them to communicate.[^2]
DNS64 describes a DNS server that when asked for a domain's AAAA
records, but only finds A records, synthesizes the AAAA records from the
A records. The first part of the synthesized IPv6 address points to an
IPv6/IPv4 translator and the second part embeds the IPv4 address from
the A record. The translator in question is usually a NAT64 server. The
standard-track specification of DNS64 is in RFC 6147.
There are two noticeable issues with this transition mechanism:
It only works for cases where DNS is used to find the remote host
address, if IPv4 literals are used the DNS64 server will never be
involved.
Because the DNS64 server needs to return records not specified by
the domain owner, DNSSEC validation against the root will fail in cases
where the DNS server doing the translation is not the domain owner's
server.[^3]
這里大概描述一下NAT64的工作流程,首先局域網(wǎng)內(nèi)有一個(gè)NAT64的路由設(shè)備并且有DNS64的服務(wù)。
客戶端進(jìn)行g(shù)etaddrinfo的域名解析.
DNS返回結(jié)果,如果返回的IP里面只有v4地址,并且當(dāng)前網(wǎng)絡(luò)是IPv6-only網(wǎng)絡(luò),DNS64服務(wù)器會(huì)把v4地址加上64:ff9b::/96的前綴,例如64:ff9b::14.17.32.211。如果當(dāng)前網(wǎng)絡(luò)是IPv4-only或IPv4-IPv6,DNS64不會(huì)做任何事情。
客戶端拿到IPv6的地址進(jìn)行connect
路由器發(fā)現(xiàn)地址的前綴為64:ff9b::/96,知道這個(gè)是NAT64的映射,是需要訪問14.17.32.211。這個(gè)時(shí)候進(jìn)行需要NAT64映射,因?yàn)榈酵饩W(wǎng)需要轉(zhuǎn)換成IPv4 stack。
當(dāng)數(shù)據(jù)返回的時(shí)候,按照NAT映射,IPv4回包重新加上前綴64:ff9b::/96,然后返回給客戶端。
//NAT64 address sample
//address init
const char* ipv6_str ="64:ff9b::14.17.32.211";
in6_addr ipv6_addr = {0};
int v6_r = inet_pton(AF_INET6, ipv6_str, &ipv6_addr);
sockaddr_in6 v6_addr = {0};
v6_addr.sin6_family = AF_INET6;
v6_addr.sin6_port = htons(80);
v6_addr.sin6_addr = ipv6_addr;
//socket connect
int v6_sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
std::string v6_error;
if (0 != connect(v6_sock, (sockaddr*)&v6_addr, 28))
{
v6_error = strerror(errno);
}
//get local ip
sockaddr_in6 v6_local_addr = {0};
socklen_t v6_local_addr_len = 28;
char v6_str_local_addr[64] = {0};
getpeername(v6_sock, (sockaddr*)&v6_local_addr, &v6_local_addr_len);
inet_ntop(v6_local_addr.sin6_family, &v6_local_addr.sin6_addr, v6_str_local_addr, 64);
close(v6_sock);
這里討論下比較坑的地方,按照NAT64的規(guī)則,客戶端如果沒有做DNS域名解析的話(微信依賴的是自己實(shí)現(xiàn)的NEWDNS),客戶端就需要完成DNS64的工作。這里的關(guān)鍵點(diǎn)是,發(fā)現(xiàn)網(wǎng)絡(luò)是IPv6-only的NAT64網(wǎng)絡(luò)的情況下,我們可以自己補(bǔ)充上前綴64:ff9b::/96,然后進(jìn)行正常的訪問。然而這里客戶端能獲取的信息量一般都是很有限的,怎么樣處理這個(gè)問題,后面有專門的章節(jié)來處理這個(gè)問題(判斷客戶端支持的IP stack)。
這里一般connect的時(shí)候會(huì)返回錯(cuò)誤碼network is unreachable,因?yàn)楦緵]有v6的協(xié)議棧,就像沒有硬件設(shè)備一樣,但是不排除會(huì)有系統(tǒng)會(huì)返回no route to host。當(dāng)然,如果服務(wù)器的地址是Teredo tunneling 2001::/32,可以客戶端直接做隧道。如果是6to4 2002::/16,并且客戶端有RAW socket權(quán)限加上非NAT網(wǎng)絡(luò),這種情況下可以客戶端自己做6to4的路由。(這里的結(jié)論不一定百分百正確,還需要繼續(xù)研讀RFC)。
v6 ip + IPv6-only or IPv4-IPv6
這里只要沒有配置上,是可以直接通訊的。當(dāng)然這里會(huì)涉及到一個(gè)問題,如果DNS返回上文說的6to4或Teredo tunneling或pure native IPv6 addresses,這樣的情況下我們?cè)趺礃幼鯥P的選擇呢,這個(gè)可以參照RFC 3484 - Default Address Selection for Internet Protocol version 6 (IPv6)。
判斷客戶端可用的IP stack(IPv4-only、 IPv6-only、IPv4-IPv6 Dual stack)
原理大家都明白了,但是客戶端做不同的處理的前提是需要知道客戶端可用的IP協(xié)議棧。
我們先定義客戶端可用的IP協(xié)議棧的意思是,獲取客戶端當(dāng)前能使用的IP協(xié)議棧。例如iOS在NAT64 WIFI連接上的情況下,Mobile的網(wǎng)卡雖然存在IPv4的協(xié)議棧,但是系統(tǒng)是不允許使用的。IOS只能使用WIFI的協(xié)議棧,在NAT64 WIFI的情況下就是IPv6-only網(wǎng)絡(luò)了。
這里還有一個(gè)問題需要討論,如果遇到IPv6-only網(wǎng)絡(luò),需要把它當(dāng)作NAT64來處理,在v4 IP前添加前綴64:ff9b::/96。
但是這里NAT64和IPv6-only不是等價(jià)的。IPv6-only網(wǎng)絡(luò)可能支持NAT64,能訪問v4的互聯(lián)網(wǎng)資源,但是IPv6-only能訪問v6的互聯(lián)網(wǎng)資源,不支持NAT64。這里假設(shè)IPv6-only的網(wǎng)絡(luò)都是支持NAT64的,對(duì)v4 IP進(jìn)行64:ff9b::/96的處理。因?yàn)椴恢С諲AT64的話,微信服務(wù)器v4地址根本就不可訪問(當(dāng)然如果手機(jī)系統(tǒng)有464XLAT服務(wù),并且運(yùn)營商支持,也是可以訪問v4資源的,但是不在討論范圍了)。
IOS通過sysctl獲取當(dāng)前網(wǎng)關(guān)或路由
如果只能獲取IPv6網(wǎng)關(guān),那當(dāng)前是IPv6-only
如果只能獲取IPv4網(wǎng)關(guān),那當(dāng)前是IPv4-only
如果同時(shí)能獲取IPv6/IPv4路由,那情況就比復(fù)雜,分析如下
IOS在WIFI連接上的情況下,并不會(huì)關(guān)閉Mobile的網(wǎng)卡。
在WIFI是IPv6-only網(wǎng)絡(luò),Mobile是IPv4-only網(wǎng)絡(luò),下v4 socket或者v4-mapped都無法出去。
證明apple應(yīng)該對(duì)TCP connect函數(shù)進(jìn)行過改造,在WIFI和Mobile共存的情況下,只能走WIFI網(wǎng)絡(luò),和Android不一樣,iOS不是通過去掉Mobile網(wǎng)卡的方式來做。
這樣導(dǎo)致的一個(gè)有趣的特性:網(wǎng)絡(luò)切換時(shí)候如果Mobile 下建立的socket不關(guān)閉可以繼續(xù)使用Mobile網(wǎng)絡(luò)。
如果程序使用bind接口綁定到Mobile的網(wǎng)卡下,這個(gè)時(shí)候是可以使用Mobile網(wǎng)絡(luò)進(jìn)行訪問的。(這里算不算偷流量呢,當(dāng)然這里是特性,具體怎么樣應(yīng)用是程序的問題了)。
因此我們可以考慮WIFI連接了的情況下,我們只要知道網(wǎng)關(guān)是對(duì)應(yīng)那張網(wǎng)卡,就可以知道當(dāng)前是不是當(dāng)前支持的IP協(xié)議棧?
然而事情沒有那么簡(jiǎn)單,我們先按照剛剛說的思路走下去
通過getifaddr接口,可以拿到當(dāng)前全部網(wǎng)絡(luò)的IP地址(排除掉非活躍和loopback的網(wǎng)卡)
如果IPv4、IPv6網(wǎng)關(guān)都屬于WIFI網(wǎng)卡,那當(dāng)前是IPv4-IPv6 Dual stack
如果IPv4、IPv6網(wǎng)關(guān)都屬于Mobile網(wǎng)卡,那當(dāng)前是IPv4-IPv6 Dual stack
到這里都沒有問題,但是下面的情況呢:
如果IPv4網(wǎng)關(guān)屬于Mobile網(wǎng)卡,IPv6網(wǎng)關(guān)屬于WIFI?
如果IPv4網(wǎng)關(guān)屬于WIFI網(wǎng)卡,IPv6網(wǎng)關(guān)屬于Mobile?
這里的情況還要分開,如果是正常情況下IOS在WIFI連接后是不允許使用Mobile網(wǎng)卡的,但是iOS又有一個(gè)特性是3G熱點(diǎn)。
在這樣的情況下IOS手機(jī)本身是走M(jìn)obile網(wǎng)絡(luò)的,WIFI只是做橋接。
這個(gè)方案非常復(fù)雜,而且跟iOS平臺(tái)的系統(tǒng)實(shí)現(xiàn)強(qiáng)耦合,其他平臺(tái)必須重新實(shí)現(xiàn),后續(xù)如果iOS進(jìn)行網(wǎng)絡(luò)邏輯的更新,這里還必須修改。因此這個(gè)的方案不太建議大家用。
這里的方案是直接做DNS解析,然后判斷返回的IP有沒有帶上64:ff9b前綴來確定當(dāng)前的IP協(xié)議棧。這也是唯一能夠判斷IPv6-only網(wǎng)絡(luò)是否支持NAT64的方案。
//gateway
in6_addr addr6_gateway = {0};
if (0 != getdefaultgateway6(&addr6_gateway))
return EIPv4;
if (IN6_IS_ADDR_UNSPECIFIED(&addr6_gateway))
return EIPv4;
in_addr addr_gateway = {0};
if (0 != getdefaultgateway(&addr_gateway))
return EIPv6;
if (INADDR_NONE == addr_gateway.s_addr || INADDR_ANY == addr_gateway.s_addr )
return EIPv6;
//getaddrinfo
struct addrinfo hints, *res, *res0;
memset(&hints, 0, sizeof(hints));
hints.ai_family = PF_INET6;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_ADDRCONFIG|AI_V4MAPPED;
int error = getaddrinfo("dns.weixin.qq.com", "http", &hints, &res0);
if (0 != error) {
return EIPv4;
}
for (res = res0; res; res = res->ai_next) {
if (AF_INET6 == res->ai_addr.sa_family) {
if (is_nat64_address(((sockaddr_in6&)res->ai_addr).sin6_addr)) {
return EIPv6;
}
}
}
return EIPv4;
我們分析下上面的sample,前面gateway的代碼是為了加速判斷過程,我們知道DNS是一個(gè)網(wǎng)絡(luò)過程,耗時(shí)很有可能是非常久的。dns.weixin.qq.com必須保證解析的域名只有v4 ip地址。hints.ai_family = PF_INET6利用了DNS64的特性,如果在純IPv6環(huán)境下會(huì)返回NAT64映射地址的方式。AI_V4MAPPED為了在非DNS64網(wǎng)絡(luò)下,返回v4-mapped ipv6 address,不會(huì)返回EAI_NONAME失敗,導(dǎo)致判斷不準(zhǔn)確。AI_ADDRCONFIG返回的地址是本地能夠使用的(具體可以看文檔下面的介紹)。如果有NAT64前綴的v6地址返回,證明當(dāng)前網(wǎng)絡(luò)是IPv6-only NAT64網(wǎng)絡(luò)。
不過這個(gè)方案有很多缺點(diǎn),就是耗時(shí)不確定,可能因?yàn)榫W(wǎng)絡(luò)失敗導(dǎo)致錯(cuò)誤的結(jié)果,需要網(wǎng)絡(luò)流量,會(huì)對(duì)運(yùn)營商的DNS服務(wù)器造成壓力,網(wǎng)絡(luò)切換需要立刻進(jìn)行重試重連。
結(jié)論,這個(gè)方案不太合適。
socket connect的方式(支持iOS9和Android)
這里的方案是直接使用v4 IP地址和v6 IP地址進(jìn)行連接,通過結(jié)果來確認(rèn)當(dāng)前客戶端可用IP stack。
_test_connect(int pf, struct sockaddr *addr, size_t addrlen) {
int s = socket(pf, SOCK_STREAM, IPPROTO_TCP);
if (s < 0)
return 0;
int ret;
do {
ret = connect(s, addr, addrlen);
} while (ret < 0 && errno == EINTR);
int success = errno;
do {
ret = close(s);
} while (ret < 0 && errno == EINTR);
return success;
}
static int
_have_ipv6() {
static const struct sockaddr_in6 sin6_test = {
.sin6_len = sizeof(sockaddr_in6),
.sin6_family = AF_INET6,
.sin6_port = htons(0xFFFF),
.sin6_addr.s6_addr = {
0, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
sockaddr_union addr = { .in6 = sin6_test };
return _test_connect(PF_INET6, &addr.generic, sizeof(addr.in6));
}
static int
_have_ipv4() {
static const struct sockaddr_in sin_test = {
.sin_len = sizeof(sockaddr_in),
.sin_family = AF_INET,
.sin_port = htons(0xFFFF),
.sin_addr.s_addr = htonl(0x08080808L),? // 8.8.8.8
};
sockaddr_union addr = { .in = sin_test };
return _test_connect(PF_INET, &addr.generic, sizeof(addr.in));
}
enum TLocalIPStack {
ELocalIPStack_None = 0,
ELocalIPStack_IPv4 = 1,
ELocalIPStack_IPv6 = 2,
ELocalIPStack_Dual = 3,
};
void test() {
TLocal IPlocal_stack = 0;
int errno_ipv4 = _have_ipv4();
int errno_ipv6 = _have_ipv6();
int local_stack = 0;
if ( errno_ipv4 != EHOSTUNREACH && errno_ipv4 != ENETUNREACH) {
local_stack |= ELocalIPStack_IPv4;
}
if (errno_ipv6 != EHOSTUNREACH && errno_ipv6 != ENETUNREACH) {
local_stack |= ELocalIPStack_IPv6;
}
}
這個(gè)方案是利用外網(wǎng)IP進(jìn)行連接,如果返回EHOSTUNREACH的時(shí)候說明本地沒有對(duì)應(yīng)的路由到達(dá)目標(biāo)地址,如果ENETUNREACH的時(shí)候說明本地沒有相應(yīng)的協(xié)議棧,這兩種情況都是說明相應(yīng)的協(xié)議棧不可用。
分析下這個(gè)方案的缺點(diǎn),和getaddrinfo一樣,耗時(shí)不確定,因?yàn)橛姓{(diào)用connect動(dòng)作,進(jìn)行tcp連接。如果connect遇到EHOSTUNREACH ENETUNREACH錯(cuò)誤是不會(huì)耗費(fèi)流量和立刻返回的,因?yàn)檫@些都是本地網(wǎng)絡(luò)判斷。但是,如果相應(yīng)網(wǎng)絡(luò)可用,這個(gè)是要花費(fèi)網(wǎng)絡(luò)流量的,耗時(shí)也不能確定。如果我們連接一個(gè)存在的IP,這樣在網(wǎng)絡(luò)好的時(shí)候很快返回(這樣會(huì)對(duì)服務(wù)器造成連接的壓力),網(wǎng)絡(luò)差的時(shí)候很久才返回。如果連接一個(gè)不存在的IP,需要很久時(shí)間才會(huì)返回(75s的連接超時(shí))。
這樣看來,這三個(gè)方案都不完美,根本不能在真實(shí)場(chǎng)景中使用, 有沒有更加可用的方案呢?iOS 9.0 上層Objc Framework可以無縫支持,但是用bsd socket需要代碼完成對(duì)應(yīng)的工作。但是iOS Framework的最新源碼也沒有開源出來,無法知道其實(shí)現(xiàn)原理。
繼續(xù)研究發(fā)現(xiàn),getaddrinfo的AI_ADDRCONFIGflags有點(diǎn)像我們需要實(shí)現(xiàn)的功能,要去掉IP,就必須要知道當(dāng)前的IP stack。它是怎么樣實(shí)現(xiàn)的?
//Android的AI_ADDRCONFIG 功能的sample
_test_connect(int pf, struct sockaddr *addr, size_t addrlen) {
int s = socket(pf, SOCK_DGRAM, IPPROTO_UDP);
if (s < 0)
return 0;
int ret;
do {
ret = connect(s, addr, addrlen);
} while (ret < 0 && errno == EINTR);
int success = (ret == 0);
do {
ret = close(s);
} while (ret < 0 && errno == EINTR);
return success;
}
static int
_have_ipv6() {
static const struct sockaddr_in6 sin6_test = {
.sin6_len = sizeof(sockaddr_in6),
.sin6_family = AF_INET6,
.sin6_port = htons(0xFFFF),
.sin6_addr.s6_addr = {
0x20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
sockaddr_union addr = { .in6 = sin6_test };
return _test_connect(PF_INET6, &addr.generic, sizeof(addr.in6));
}
static int
_have_ipv4() {
static const struct sockaddr_in sin_test = {
.sin_len = sizeof(sockaddr_in),
.sin_family = AF_INET,
.sin_port = htons(0xFFFF),
.sin_addr.s_addr = htonl(0x08080808L),? // 8.8.8.8
};
sockaddr_union addr = { .in = sin_test };
return _test_connect(PF_INET, &addr.generic, sizeof(addr.in));
}
enum TLocalIPStack {
ELocalIPStack_None = 0,
ELocalIPStack_IPv4 = 1,
ELocalIPStack_IPv6 = 2,
ELocalIPStack_Dual = 3,
};
void test() {
TLocal IPlocal_stack = 0;
int have_ipv4 = _have_ipv4();
int have_ipv6 = _have_ipv6();
int local_stack = 0;
if ( have_ipv4) {
local_stack |= ELocalIPStack_IPv4;
}
if (have_ipv6) {
local_stack |= ELocalIPStack_IPv6;
}
}
這里的代碼構(gòu)造成和第一個(gè)socket connect的類型,只修改一部分代碼。可以看到,和第一個(gè)例子的區(qū)別是socket(pf, SOCK_DGRAM, IPPROTO_UDP)用了UDP進(jìn)行連接,UDP可以進(jìn)行connect,只是進(jìn)行綁定服務(wù)器地址的動(dòng)作,并不會(huì)有網(wǎng)絡(luò)數(shù)據(jù)的產(chǎn)生,后續(xù)可以直接使用send接口,不需要使用sendto接口(每次都需指定服務(wù)器的地址)。
經(jīng)過測(cè)試iOS和Android都能檢測(cè)出當(dāng)前可用的IP stack。我們?cè)僮鲆恍┧伎迹绻鹀onnect接口在UDP的時(shí)候,應(yīng)該是除了TCP發(fā)送syn包外的全部事情都做了的。如果這樣考慮的話,這個(gè)方案成立的依據(jù)還是足夠的。
混合的方案(Mac OS,iOS,Linux,Android都支持,Windows/wp待測(cè)試)
發(fā)現(xiàn)在iOS8/Mac OS上述方案會(huì)有點(diǎn)問題(iOS9正常),就是iOS8上IPv6-only網(wǎng)絡(luò)也會(huì)有169.254.x.x的自組網(wǎng)的IPv4 stack(其實(shí)iOS9上也有,但不影響測(cè)試結(jié)果),這樣會(huì)導(dǎo)致IPv4 stack的udp socket能夠connect成功(_have_ipv4()返回1)。應(yīng)對(duì)這種情況,我們可以用前面getdefaultgateway的方案,把自組網(wǎng)排除出沒有網(wǎng)關(guān)的情況。當(dāng)然,有手機(jī)網(wǎng)的時(shí)候,IPv4網(wǎng)關(guān)是可以獲取到的,還是會(huì)走到_have_ipv4的路徑。當(dāng)然,如果have_ipv4和have_ipv6只有一個(gè)返回1的情況,我們可以認(rèn)為只有一個(gè)IP stack能用。當(dāng)然如果是local_stack為ELocalIPStack_Dual,還需要用getdnssvraddrs的函數(shù)獲取當(dāng)前的dns服務(wù)器列表,通過dns服務(wù)器的地址確認(rèn)當(dāng)前可用的IP stack。必須說明下,這個(gè)不是一個(gè)準(zhǔn)確的判斷,如果網(wǎng)絡(luò)是ELocalIPStack_Dual,但是dns服務(wù)只設(shè)置了IPv6的地址(如果是dhcp配置的情況,很少出現(xiàn)這樣,一般情況都是手工設(shè)置才會(huì)出現(xiàn)),會(huì)判斷當(dāng)前網(wǎng)絡(luò)為ELocalIPStack_IPv6。這樣ELocalIPStack_Dual的網(wǎng)絡(luò)可能不支持NAT64,這樣會(huì)導(dǎo)致程序無法訪問網(wǎng)絡(luò)。
這個(gè)方案是本地操作,成本低,沒有網(wǎng)絡(luò)流量消耗和耗時(shí)問題,暫時(shí)是最好的可用IP stack檢測(cè)方案。(當(dāng)然NAT64檢測(cè)不了)新的實(shí)現(xiàn)代碼如下:
TLocalIPStack local_ipstack_detect() {
in6_addr addr6_gateway = {0};
if (0 != getdefaultgateway6(&addr6_gateway)){ return ELocalIPStack_IPv4;}
if (IN6_IS_ADDR_UNSPECIFIED(&addr6_gateway)) { return ELocalIPStack_IPv4;}
in_addr addr_gateway = {0};
if (0 != getdefaultgateway(&addr_gateway)) { return ELocalIPStack_IPv6;}
if (INADDR_NONE == addr_gateway.s_addr || INADDR_ANY == addr_gateway.s_addr ) { return ELocalIPStack_IPv6;}
int have_ipv4 = _have_ipv4();
int have_ipv6 = _have_ipv6();
int local_stack = 0;
if (have_ipv4) { local_stack |= ELocalIPStack_IPv4; }
if (have_ipv6) { local_stack |= ELocalIPStack_IPv6; }
if (ELocalIPStack_Dual != local_stack) { return (TLocalIPStack)local_stack; }
int dns_ip_stack = 0;
std::vector dnssvraddrs;
getdnssvraddrs(dnssvraddrs);
for (int i = 0; i < dnssvraddrs.size(); ++i) {
if (AF_INET == dnssvraddrs[i].address().sa_family) { dns_ip_stack |= ELocalIPStack_IPv4; }
if (AF_INET6 == dnssvraddrs[i].address().sa_family) { dns_ip_stack |= ELocalIPStack_IPv6; }
}
return (TLocalIPStack)(ELocalIPStack_None==dns_ip_stack? local_stack:dns_ip_stack);
}
建議大家認(rèn)真看apple的文檔Supporting IPv6 DNS64/NAT64 Networks和RFC 4038 - Application Aspects of IPv6 Transition,里面很多事情都說清楚了,這里說下其他需要關(guān)注的地方。
sockaddr的存儲(chǔ)sockaddr_storage
這里千萬不要犯傻用sockaddr存儲(chǔ)sockaddr_in6數(shù)據(jù),IOS上sockaddr的大小是16,和sockaddr_in一致的,但是sockaddr_in6大小是28(不要問我為什么會(huì)知道,都是淚)。通用的sockaddr的存儲(chǔ)的結(jié)構(gòu)體是sockaddr_storage,它是能存儲(chǔ)任何sockaddr的結(jié)構(gòu)。你可能會(huì)問,如果socket用AF_INET6的時(shí)候,用sockaddr_in6結(jié)構(gòu)體不就好了。不是說不可以,就是代碼會(huì)變成IPv6專用的了,如果用到其他地方可能會(huì)出錯(cuò)。但是如果用AF_INET呢,雖然強(qiáng)轉(zhuǎn)成sockaddr_in沒有任何問題,但是程序邏輯上蛋疼,如果大家要寫v4/v6通用的邏輯的話,最好還是用sockaddr_storage存儲(chǔ),然后通過ss_family進(jìn)行判斷,最后做不同分支的處理。
//sockaddr_storage sample
socket_address socket_address::getsockname(SOCKET _sock)
{
struct sockaddr_storage addr = {0};
socklen_t addr_len = sizeof(addr);
::getsockname(_sock, (sockaddr*)&addr, &addr_len);
if (AF_INET == addr.ss_family)
{
return socket_address((const sockaddr_in&)addr);
}
else if (AF_INET6 == addr.ss_family)
{
return socket_address((const sockaddr_in6&)addr);
}
return socket_address("", 0);
}
sockaddr_storage是能夠保存所有sockaddr下屬的類型,但是128字節(jié)的大小有時(shí)候有點(diǎn)不可接受,而且每次使用都需要做類型轉(zhuǎn)換。下面提供一個(gè)更加優(yōu)雅的方案,大小是28字節(jié),節(jié)省了很多。
union sockaddr_union {
struct sockaddr? ? sa;
struct sockaddr_in? in;
struct sockaddr_in6 in6;
} m_addr;
if (AF_INET == m_addr.sa.sa_family) {
return ntohs(m_addr.in.sin_port);
} else if (AF_INET6 == m_addr.sa.sa_family) {
return ntohs(m_addr.in6.sin6_port);
}
apple要求大家不要直接用IP訪問,不過,中國的DNS環(huán)境這么惡劣,沒有其他更好的辦法。那NSURLConnection怎么樣能夠在IPv6訪問正常的訪問呢?我們應(yīng)該構(gòu)建怎么樣的URL呢?
我們先看看wikipedia的說法
Literal IPv6 addresses in network resource identifiers[^4]
Colon (:) characters in IPv6 addresses may conflict with the
established syntax of resource identifiers, such as URIs and URLs. The
colon has traditionally been used to terminate the host path before a
port number.[6] To alleviate this conflict, literal IPv6 addresses are
enclosed in square brackets in such resource identifiers, for example:
http://[2001:db8:85a3:8d3:1319:8a2e:370:7348]/
When the URL also contains a port number the notation is:
https://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443/
我們可以看到除了IP形式不一樣外,還多了中括號(hào)。當(dāng)然,上面我們說到IPv4-mapped IPv6 address和NAT64 mapped address同樣也是適用的,例如:http://[::ffff:14.17.32.21]:80走IPv4協(xié)議棧或http://[64:ff9b::14.17.32.21]走NAT64。關(guān)鍵點(diǎn)還是判斷當(dāng)前客戶端可用的IP stack。
IOS下CoreFoudation或者更高級(jí)的API
引用手Q同事的原話:
如果使用CoreFoudation或者更高級(jí)的API,即使在純IPv6環(huán)境下使用IPv4的ip進(jìn)行網(wǎng)絡(luò)通信,iOS9會(huì)自動(dòng)把IPv4地址轉(zhuǎn)換成IPv6地址。換句話說,因?yàn)槭謖里面大部分api都是滿足要求的,基本上不用改動(dòng)。(注意iOS9.0或以上)
例如CFStreamCreatePairWithSocketToCFHost ..待續(xù)
apple的文檔說了,gethostbyname這些已經(jīng)不能用了(只支持IPv4),只能用getaddrinfo。
struct addrinfo hints, *res, *res0;
memset(&hints, 0, sizeof(hints));
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_DEFAULT;
getaddrinfo("www.qq.com", "http", &hints, &res0);
這里sample比較簡(jiǎn)單,其實(shí)getaddrinfo的重點(diǎn)在hints.ai_family和hints.ai_flags的設(shè)置上,apple已經(jīng)給出了一個(gè)很好sample。我們分析下這兩個(gè)變量不同設(shè)置下的效果,看看有什么區(qū)別。
hints.ai_family = PF_UNSPEC的意思是v4地址和v6地址都返回,不過呢,這里可是會(huì)觸發(fā)兩個(gè)UDP的請(qǐng)求,當(dāng)年微信就給運(yùn)營商吐槽過,你沒有v6地址,就不要做v6請(qǐng)求拉(微信量大)。不過apple爸爸要求用v6地址,怎么辦?
hints.ai_family = PF_INET的意思是只返回v4地址
hints.ai_family = PF_INET6的意思是只返回v6地址
hints.ai_flags |= AI_V4MAPPED 且 hints.ai_family = PF_INET6的情況下,如果需要dns的host沒有v6地址的情況下,getaddinfo會(huì)把v4地址轉(zhuǎn)換成v4-mapped ipv6 address,如果有v6地址返回就不會(huì)做任何動(dòng)作。
hints.ai_flags |= AI_ADDRCONFIG這個(gè)是一個(gè)很有用的特性,這個(gè)flags表示getaddrinfo會(huì)根據(jù)本地網(wǎng)絡(luò)情況,去掉不支持的IP協(xié)議地址。
hints.ai_flags = AI_DEFAULT其實(shí)就是AI_V4MAPPED|AI_ADDRCONFIG_CFG,也是apple推薦的flags設(shè)置方式。
域名? 對(duì)應(yīng)著如下 IP 地址:
173.194.127.180
173.194.127.176
2404:6800:4005:802::1010
若本地主機(jī)僅配置了 IPV4 地址,則返回的查詢結(jié)果中不包含 IPV6 地址,即此時(shí)只有:
173.194.127.180
173.194.127.176
同樣若本地主機(jī)僅配置了 IPV6 地址,則返回的查詢結(jié)果中僅包含IPV6地址.2404:6800:4005:802::1010
用這個(gè)API的時(shí)候,建議大家還是按照apple的sample來做hints.ai_family暫時(shí)先PF_INET,免得運(yùn)營商投訴,當(dāng)然最好是能后臺(tái)進(jìn)行控制。
下面一段話是apple文檔內(nèi)對(duì)getaddrinfo對(duì)NAT64支持的描述。
The current implementation supports synthesis of NAT64 mapped IPv6
addresses.? If hostname is a numeric string defining an IPv4 address
(for example, '192.0.2.1' ) and ai_family is set to PF_UNSPEC or
PF_INET6, getaddrinfo() will synthesize the appropriate IPv6 address(es)
(for example, '64:ff9b::192.0.2.1' ) if the current interface supports
IPv6, NAT64 and DNS64 and does not support IPv4. If the AI_ADDRCONFIG
flag is set, the IPv4 address will be suppressed on those interfaces.
On non-qualifying interfaces, getaddrinfo() is guaranteed to return
immediately without attempting any resolution, and will return the IPv4
address if ai_family is PF_UNSPEC or PF_INET. NAT64 address synthesis
can be disabled by setting the AI_NUMERICHOST flag. To best support
NAT64 networks, it is recommended to resolve all IP address literals
with ai_family set to PF_UNSPEC and ai_flags set to AI_DEFAULT.
可以看到apple最推薦的getaddrinfo用法就是sample那樣。
iOS SCNetworkReachabilityCreateWithAddress API問題
在iOS下,一般判斷網(wǎng)絡(luò)連通性和WIFI/Mobile網(wǎng)絡(luò)的判斷是使用
SCNetworkReachabilityCreateWithAddress
API,一般的sample里面只會(huì)測(cè)試IPv4的IP地址,這樣有可能導(dǎo)致在純IPv6網(wǎng)絡(luò)下判斷出當(dāng)前是沒有網(wǎng)絡(luò),這樣明顯是不對(duì)的。
struct sockaddr_in zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;
SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)zeroAddress);
針對(duì)IPv6可以使用下面的方式來判斷
struct sockaddr_in6 zeroAddress6;
bzero(&zeroAddress6, sizeof(zeroAddress6));
zeroAddress6.sin6_len = sizeof(zeroAddress6);
zeroAddress6.sin6_family = AF_INET6;
SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)zeroAddress6);
當(dāng)然大家寫通用代碼的時(shí)候,可以IPv6和IPv4都判斷。
最后蘋果建議的方式是SCNetworkReachabilityCreateWithName這個(gè)API,個(gè)人暫時(shí)不確定這個(gè)API是不是會(huì)進(jìn)行DNS解析。
Beej's Guide to Network Programming 簡(jiǎn)體中文這本書不錯(cuò)的,介紹了很多API的使用,當(dāng)然IPv6部分也有
Beej's Guide to Network Programming 繁體中文排版比簡(jiǎn)體好
unix network programming 不用說了,不過沒有v6的部分
Dual-Stack Sockets for IPv6 Winsock Applications(Windows XP SP1后都支持)
gethostbyname()
gethostbyaddr()
getservbyname()
getservbyport()
gethostbyname2()
inet_addr()
inet_aton()
inet_lnaof()
inet_makeaddr()
inet_netof()
inet_network()
inet_ntoa()
inet_ntoa_r()
bindresvport()
getipv4sourcefilter()
setipv4sourcefilter()
下面類型或者結(jié)構(gòu)需要注意使用的正確性
IPv4IPv6
AF_INETAF_INET6
PF_INETPF_INET6
struct in_addrstruct in_addr6
struct sockaddr_instruct sockaddr_in6
kDNSServiceProtocol_IPv4kDNSServiceProtocol_IPv6
[^1]:wikipedia Transition from IPv4[^2]:wikipedia NAT64[^3]:wikipedia DNS64[^4]:wikipedia IPv6 address