引言
最近公司的App不能提交到App Store了做裙。因為,我們的app無法支持NAT64網(wǎng)絡(luò)環(huán)境沉迹。蘋果還算是比較人性化,給出了解決方案 Supporting IPv6 DNS64/NAT64 Networks枣耀。這篇文章說得很詳細(xì),大致意思就是盡量使用域名而不是ip地址訪問服務(wù)器庭再,主要調(diào)用了 getaddrinfo
這個方法捞奕。他會根據(jù)你給的選項及當(dāng)前所處的網(wǎng)絡(luò)環(huán)境,返回一個ip地址列表拄轻。當(dāng)然如果是NAT64網(wǎng)絡(luò)颅围,也會包含ipv6地址(如果域名綁定了ipv6地址,則返回服務(wù)器的ipv6地址恨搓;如果服務(wù)器只有ipv4地址院促,則它會自動幫你合成一個ipv6地址)。這樣會省去你很多麻煩斧抱。
我們的app直接使用low-level BSD socket api通過tcp協(xié)議直接與后臺服務(wù)器通信常拓。服務(wù)器只有ipv4地址,并且沒有綁定域名辉浦。剛開始我們嘗試使用蘋果給出的示例代碼解決該問題:
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <err.h>
uint8_t ipv4[4] = {192, 0, 2, 1};
struct addrinfo hints, *res, *res0;
int error, s;
const char *cause = NULL;
char ipv4_str_buf[INET_ADDRSTRLEN] = { 0 };
const char *ipv4_str = inet_ntop(AF_INET, &ipv4, ipv4_str_buf, sizeof(ipv4_str_buf));
memset(&hints, 0, sizeof(hints));
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_DEFAULT;
error = getaddrinfo(ipv4_str, "http", &hints, &res0);
if (error) {
errx(1, "%s", gai_strerror(error));
/*NOTREACHED*/
}
s = -1;
for (res = res0; res; res = res->ai_next) {
s = socket(res->ai_family, res->ai_socktype,
res->ai_protocol);
if (s < 0) {
cause = "socket";
continue;
}
if (connect(s, res->ai_addr, res->ai_addrlen) < 0) {
cause = "connect";
close(s);
s = -1;
continue;
}
break; /* okay we got one */
}
if (s < 0) {
err(1, "%s", cause);
/*NOTREACHED*/
}
freeaddrinfo(res0);
看起來很簡單: 將我們想要合成的ipv4地址弄抬、端口及選項傳給getaddrinfo就可以得到一個地址鏈表;然后宪郊,我們測試地址的連通性掂恕,在其中找到一個可用的拖陆。
但是,經(jīng)過測試發(fā)現(xiàn)懊亡,getaddrinfo針對域名可以工作依啰,但是對于ipv4地址卻不能合成ipv6地址。這非常奇怪店枣,我查閱了兩份關(guān)于IPv6的RFC文檔: RFC4038: Application Aspects of IPv6 Transition和rfc7050: Discovery of the IPv6 Prefix Used for IPv6 Address Synthesis速警;也在蘋果官方論壇上咨詢了,他們給的答復(fù)是可以正常工作鸯两,并且建議使用域名闷旧。。甩卓。但是我這邊確實是不可以的啊鸠匀,相同的代碼蕉斜,而且后臺服務(wù)器也沒域名逾柿。在申請到新域名之前,作為過渡方案宅此,沒辦法机错,只能另辟蹊徑了。
前面提到 getaddrinfo
對于域名可以正確地工作父腕。我們?yōu)槭裁床焕眠@一點(diǎn)呢弱匪?
原理
首先先針對一個比較知名的域名調(diào)用getaddrinfo得到ip地址列表,然后針對地址列表進(jìn)行進(jìn)一步處理璧亮,用我們的ip地址替換ip地址列表中的地址萧诫。在rfc7050: Discovery of the IPv6 Prefix Used for IPv6 Address Synthesis這篇文章中,提到了一個非常特殊的域名 ipv4only.arpa., 該域名只綁定了兩個ipv4地址枝嘶。這非常好帘饶,因為我們無需擔(dān)心挑選的域名綁定了ipv6地址,給我們處理地址列時帶來不便,增加了問題的復(fù)雜度群扶。所以及刻,我們的思路是:
- 首先調(diào)用getaddrinfo對 ipv4only.arpa. 進(jìn)行解析,得到地址列表竞阐;
- 對地址列表進(jìn)一步進(jìn)行處理:
- ipv4地址缴饭,直接進(jìn)行處理;
- ipv6地址骆莹,將高4個字節(jié)替換成我們的ip地址颗搂。
- 刪除其中的重復(fù)項。
實現(xiàn)
函數(shù)參數(shù)與getaddrinfo一致幕垦。但是峭火,hostname是ipv4地址字符串毁习,servname是端口數(shù)字字符串。函數(shù)聲明如下:
int
getaddrinfo4ipv4literal(const char *hostname, const char *servname,
const struct addrinfo *hints, struct addrinfo **res);
- 獲取地址列表
int
getaddrinfo4ipv4literal(const char *hostname, const char *servname,
const struct addrinfo *hints, struct addrinfo **res)
{
int rlt = 0;
// 1. get address list
rlt = getaddrinfo("ipv4only.arpa", "http", hints, res);
需要注意的是卖丸,這里servname傳遞的是"http"纺且。事實證明,傳遞一個任意的端口數(shù)字字符串也是可以的稍浆,并不影響地址的解析载碌。
- 地址替換
地址替換需要注意的是,不同的地址會有不同的替換方法:ipv4直接替換衅枫;ipv6只替換地址的高4個字節(jié)嫁艇。代碼如下:
in_addr_t ipv4 = inet_addr(hostname);
in_port_t port = htons(atoi(servname));
// 2. look through the address list and replace the ipv4 address and port by ours
struct addrinfo *tempRes;
for (tempRes = *res; tempRes; tempRes = tempRes->ai_next) {
if (tempRes->ai_family == AF_INET)// IPv4
{
struct sockaddr_in *dest = (struct sockaddr_in *)tempRes->ai_addr;
// overwrite
dest->sin_addr.s_addr = ipv4;
dest->sin_port = port;
}
else if(tempRes->ai_family == AF_INET6)// IPv6
{
struct sockaddr_in6 *dest = (struct sockaddr_in6 *)tempRes->ai_addr;
// overwrite the last four bytes
memcpy(&dest->sin6_addr.__u6_addr.__u6_addr8 + 12,&ipv4, sizeof(in_addr_t));
dest->sin6_port = port;
}
- 消除重復(fù)項
因為 ipv4only.arpa. 綁定了兩個ipv4地址,所以經(jīng)過第二步的替換后弦撩,出現(xiàn)了重復(fù)項步咪。所以,我們需要稍微處理下益楼。本質(zhì)上就是對一個鏈表做刪除重復(fù)項操作猾漫。
/**
* remove duplicated items
*/
void removeduplicateditems(struct addrinfo **res)
{
struct addrinfo *current, *checking, *previousChecking;
current = *res;
while (current != NULL && current->ai_next != NULL) {
checking = current->ai_next;
previousChecking = current;
while (checking != NULL) {
if (ipequal(checking,current)) {
previousChecking->ai_next = checking->ai_next;
checking->ai_next = NULL;
freeaddrinfo(checking);
checking = previousChecking->ai_next;
}
else
{
checking = checking->ai_next;
}
}
current = current->ai_next;
}
}
原理其實很簡單。全部代碼見 GitHub感凤。
討論
雖然悯周,我們解決了該問題,但是這只是一個過渡方案陪竿。還是應(yīng)該像蘋果建議的那樣禽翼,申請一個域名,這會降低問題的復(fù)雜度族跛。另外闰挡,服務(wù)器最好分配一個IPv6地址,畢竟IPv6是大勢所趨礁哄。