标准STUN判断NAT类型的过程及改进
這里基于stund的實現(xiàn),來研究標準STUN協(xié)議,判斷NatType的過程。
stund用于判斷NatType的接口的用法
首先來看stund中用于判斷NatType的接口的用法。這里主要來看stund中的STUN客戶端client.cxx的實現(xiàn)。client.cxx是一個常規(guī)的C/C++ app,這個app的主要code如下:
void usage() {cerr << "Usage:" << endl<< " ./client stunServerHostname [testNumber] [-v] [-p srcPort] ""[-i nicAddr1] [-i nicAddr2] [-i nicAddr3] " << endl<< "For example, if the STUN server was larry.gloo.net, you could do:" << endl<< " ./client larry.gloo.net" << endl<< "The testNumber is just used for special tests." << endl<< " test 1 runs test 1 from the RFC. For example:" << endl<< " ./client larry.gloo.net 0" << endl << endl << endl; }#define MAX_NIC 3 StunAddress4 stunServerAddr;int main(int argc, char* argv[]) {assert( sizeof(UInt8 ) == 1);assert( sizeof(UInt16) == 2);assert( sizeof(UInt32) == 4);initNetwork();cout << "STUN client version " << STUN_VERSION << endl;int testNum = 0;bool verbose = false;stunServerAddr.addr = 0;int srcPort = 0;StunAddress4 sAddr[MAX_NIC];int retval[MAX_NIC];int numNic = 0;for (int i = 0; i < MAX_NIC; i++) {sAddr[i].addr = 0;sAddr[i].port = 0;retval[i] = 0;}for (int arg = 1; arg < argc; arg++) {if (!strcmp(argv[arg], "-v")) {verbose = true;} else if (!strcmp(argv[arg], "-i")) {arg++;if (argc <= arg) {usage();exit(-1);}if (numNic >= MAX_NIC) {cerr << "Can not have more than " << MAX_NIC <<" -i options" << endl;usage();exit(-1);}stunParseServerName(argv[arg], sAddr[numNic++]);} else if (!strcmp(argv[arg], "-p")) {arg++;if (argc <= arg) {usage();exit(-1);}srcPort = strtol(argv[arg], NULL, 10);} else {char* ptr;int t = strtol(argv[arg], &ptr, 10);if (*ptr == 0) {// conversion workedtestNum = t;cout << "running test number " << testNum << endl;} else {bool ret = stunParseServerName(argv[arg], stunServerAddr);if (ret != true) {cerr << argv[arg] << " is not a valid host name " << endl;usage();exit(-1);}}}}if (srcPort == 0) {srcPort = stunRandomPort();}if (numNic == 0) {// use defaultnumNic = 1;}for (int nic = 0; nic < numNic; nic++) {sAddr[nic].port = srcPort;if (stunServerAddr.addr == 0) {usage();exit(-1);}if (testNum == 0) {bool presPort = false;bool hairpin = false;NatType stype = stunNatType(stunServerAddr, verbose, &presPort, &hairpin, srcPort, &sAddr[nic]);if (nic == 0) {cout << "Primary: ";} else {cout << "Secondary: ";}switch (stype) {case StunTypeFailure:cout << "Some stun error detetecting NAT type";retval[nic] = -1;exit(-1);break;case StunTypeUnknown:cout << "Some unknown type error detetecting NAT type";retval[nic] = 0xEE;break;case StunTypeOpen:cout << "Open";retval[nic] = 0x00;break;case StunTypeIndependentFilter:cout << "Independent Mapping, Independent Filter";if (presPort)cout << ", preserves ports";elsecout << ", random port";if (hairpin)cout << ", will hairpin";elsecout << ", no hairpin";retval[nic] = 0x02;break;case StunTypeDependentFilter:cout << "Independent Mapping, Address Dependent Filter";if (presPort)cout << ", preserves ports";elsecout << ", random port";if (hairpin)cout << ", will hairpin";elsecout << ", no hairpin";retval[nic] = 0x04;break;case StunTypePortDependedFilter:cout << "Independent Mapping, Port Dependent Filter";if (presPort)cout << ", preserves ports";elsecout << ", random port";if (hairpin)cout << ", will hairpin";elsecout << ", no hairpin";retval[nic] = 0x06;break;case StunTypeDependentMapping:cout << "Dependent Mapping";if (presPort)cout << ", preserves ports";elsecout << ", random port";if (hairpin)cout << ", will hairpin";elsecout << ", no hairpin";retval[nic] = 0x08;break;case StunTypeFirewall:cout << "Firewall";retval[nic] = 0x0A;break;case StunTypeBlocked:cout << "Blocked or could not reach STUN server";retval[nic] = 0x0C;break;default:cout << stype;cout << "Unkown NAT type";retval[nic] = 0x0E; // Unknown NAT typebreak;}cout << "\t";cout.flush();if (!hairpin) {retval[nic] |= 0x10;}if (presPort) {retval[nic] |= 0x01;}} else if (testNum == 100) {可以看到這個app主要做了3件事情:
解析參數(shù)。主要從參數(shù)中獲得STUN server的地址,及本地用于發(fā)送數(shù)據(jù)包所用的UDP端口號。
調(diào)用stunNatType()函數(shù)判斷NatType。判斷NatType的全部邏輯都在這個函數(shù)里。
將stunNatType()函數(shù)返回的NatType進行格式化并打印輸出,以便于人的閱讀。
接著來看stunNatType()函數(shù)的實現(xiàn)
stunNatType()函數(shù)的實現(xiàn)
stunNatType()函數(shù)的實現(xiàn)如下:
NatType stunNatType(StunAddress4& dest, bool verbose, bool* preservePort, // if set, is return for if NAT preservers ports or notbool* hairpin, // if set, is the return for if NAT will hairpin packetsint port, // port to use for the test, 0 to choose random portStunAddress4* sAddr // NIC to use) {assert( dest.addr != 0);assert( dest.port != 0);if (hairpin) {*hairpin = false;}if (port == 0) {port = stunRandomPort();}UInt32 interfaceIp = 0;if (sAddr) {interfaceIp = sAddr->addr;}Socket myFd1 = openPort(port, interfaceIp, verbose);Socket myFd2 = openPort(port + 1, interfaceIp, verbose);if ((myFd1 == INVALID_SOCKET) || (myFd2 == INVALID_SOCKET)) {cerr << "Some problem opening port/interface to send on" << endl;return StunTypeFailure;}assert( myFd1 != INVALID_SOCKET);assert( myFd2 != INVALID_SOCKET);bool respTestI = false;bool isNat = true;StunAddress4 testImappedAddr;bool respTestI2 = false;bool mappedIpSame = true;StunAddress4 testI2mappedAddr;StunAddress4 testI2dest = dest;bool respTestII = false;bool respTestIII = false;bool respTestHairpin = false;bool respTestPreservePort = false;memset(&testImappedAddr, 0, sizeof(testImappedAddr));StunAtrString username;StunAtrString password;username.sizeValue = 0;password.sizeValue = 0;#ifdef USE_TLS stunGetUserNameAndPassword( dest, username, password ); #endifint count = 0;while (count < 7) {struct timeval tv;fd_set fdSet; #ifdef WIN32unsigned int fdSetSize; #elseint fdSetSize; #endifFD_ZERO(&fdSet);fdSetSize = 0;FD_SET(myFd1, &fdSet);fdSetSize = (myFd1 + 1 > fdSetSize) ? myFd1 + 1 : fdSetSize;FD_SET(myFd2, &fdSet);fdSetSize = (myFd2 + 1 > fdSetSize) ? myFd2 + 1 : fdSetSize;tv.tv_sec = 0;tv.tv_usec = 150 * 1000; // 150 msif (count == 0)tv.tv_usec = 0;int err = select(fdSetSize, &fdSet, NULL, NULL, &tv);int e = getErrno();if (err == SOCKET_ERROR) {// error occuredcerr << "Error " << e << " " << strerror(e) << " in select" << endl;return StunTypeFailure;} else if (err == 0) {// timeout occuredcount++;if (!respTestI) {stunSendTest(myFd1, dest, username, password, 1, verbose);}if ((!respTestI2) && respTestI) {// check the address to send to if validif ((testI2dest.addr != 0) && (testI2dest.port != 0)) {stunSendTest(myFd1, testI2dest, username, password, 10, verbose);}}if (!respTestII) {stunSendTest(myFd2, dest, username, password, 2, verbose);}if (!respTestIII) {stunSendTest(myFd2, dest, username, password, 3, verbose);}if (respTestI && (!respTestHairpin)) {if ((testImappedAddr.addr != 0) && (testImappedAddr.port != 0)) {stunSendTest(myFd1, testImappedAddr, username, password, 11, verbose);}}} else {//if (verbose) clog << "-----------------------------------------" << endl;assert( err>0);// data is avialbe on some fdfor (int i = 0; i < 2; i++) {Socket myFd;if (i == 0) {myFd = myFd1;} else {myFd = myFd2;}if (myFd != INVALID_SOCKET) {if (FD_ISSET(myFd,&fdSet)) {char msg[STUN_MAX_MESSAGE_SIZE];int msgLen = sizeof(msg);StunAddress4 from;getMessage(myFd, msg, &msgLen, &from.addr, &from.port, verbose);StunMessage resp;memset(&resp, 0, sizeof(StunMessage));stunParseMessage(msg, msgLen, resp, verbose);if (verbose) {clog << "Received message of type " << resp.msgHdr.msgType << " id="<< (int) (resp.msgHdr.id.octet[0]) << endl;}switch (resp.msgHdr.id.octet[0]) {case 1: {if (!respTestI) {testImappedAddr.addr = resp.mappedAddress.ipv4.addr;testImappedAddr.port = resp.mappedAddress.ipv4.port;respTestPreservePort = (testImappedAddr.port == port);if (preservePort) {*preservePort = respTestPreservePort;}testI2dest.addr = resp.changedAddress.ipv4.addr;if (sAddr) {sAddr->port = testImappedAddr.port;sAddr->addr = testImappedAddr.addr;}count = 0;}respTestI = true;}break;case 2: {respTestII = true;}break;case 3: {respTestIII = true;}break;case 10: {if (!respTestI2) {testI2mappedAddr.addr = resp.mappedAddress.ipv4.addr;testI2mappedAddr.port = resp.mappedAddress.ipv4.port;mappedIpSame = false;if ((testI2mappedAddr.addr == testImappedAddr.addr)&& (testI2mappedAddr.port == testImappedAddr.port)) {mappedIpSame = true;}}respTestI2 = true;}break;case 11: {if (hairpin) {*hairpin = true;}respTestHairpin = true;}break;}}}}}}// see if we can bind to this address//cerr << "try binding to " << testImappedAddr << endl;Socket s = openPort(0/*use ephemeral*/, testImappedAddr.addr, false);if (s != INVALID_SOCKET) {closesocket(s);isNat = false;//cerr << "binding worked" << endl;} else {isNat = true;//cerr << "binding failed" << endl;}if (verbose) {clog << "test I = " << respTestI << endl;clog << "test II = " << respTestII << endl;clog << "test III = " << respTestIII << endl;clog << "test I(2) = " << respTestI2 << endl;clog << "is nat = " << isNat << endl;clog << "mapped IP same = " << mappedIpSame << endl;clog << "hairpin = " << respTestHairpin << endl;clog << "preserver port = " << respTestPreservePort << endl;}#if 0// implement logic flow chart from draft RFCif (respTestI) {if (isNat) {if (respTestII) {return StunTypeConeNat;} else {if (mappedIpSame) {if (respTestIII) {return StunTypeRestrictedNat;} else {return StunTypePortRestrictedNat;}} else {return StunTypeSymNat;}}} else {if (respTestII) {return StunTypeOpen;} else {return StunTypeSymFirewall;}}} else {return StunTypeBlocked;} #elseif (respTestI) { // not blockedif (isNat) {if (mappedIpSame) {if (respTestII) {return StunTypeIndependentFilter;} else {if (respTestIII) {return StunTypeDependentFilter;} else {return StunTypePortDependedFilter;}}} else { // mappedIp is not samereturn StunTypeDependentMapping;}} else { // isNat is falseif (respTestII) {return StunTypeOpen;} else {return StunTypeFirewall;}}} else {return StunTypeBlocked;} #endifreturn StunTypeUnknown; }可以看到這個函數(shù)主要做了幾件事:
打開了兩個UDP socket。后續(xù)會通過這兩個socket來進行數(shù)據(jù)包的發(fā)送,并最終根據(jù)這些數(shù)據(jù)包的響應數(shù)據(jù)包的情況來判斷NatType。
向STUN server發(fā)送請求。調(diào)用stunSendTest()函數(shù)發(fā)送了5種不同類型的消息,各個消息之間的差異也僅僅在與stunSendTest()函數(shù)的testNum參數(shù)不同。這里我們也用testNum來區(qū)分不同的消息,我們稱它們分別為類型1,類型2,類型3,類型10及類型11的消息。
其中類型10和類型11的消息依賴于類型1的消息的響應,但類型2和類型3的消息的發(fā)送則與類型1的消息的發(fā)送及響應相互獨立,因而它們可以與類型1的消息并行的發(fā)送。
接收發(fā)送的消息的響應。
從類型1的消息的響應中獲得的東西比較多。類型10和類型11的消息要發(fā)送的目標地址,都來源于類型1的消息的響應。
類型10的消息發(fā)向類型1的消息的響應的changedAddress地址。這個地址是STUN server的副IP地址及端口號。
類型11的消息則發(fā)向類型1的消息的響應的testImappedAddr地址,這個地址是發(fā)送消息的地址的出口公網(wǎng)地址,向這個消息發(fā)送消息實際是向本節(jié)點在發(fā)送消息,這么做的實際目的是為了測試節(jié)點所連接的NAT是否支持消息的回傳,或者說測試NAT是否是hairpin的。即如果這個類型11的消息通過NAT并最終被發(fā)送給本節(jié)點且本節(jié)點接收到了這個消息,則說明本節(jié)點所連接的NAT是hairpin的。
STUN終端會從類型10的消息的響應中獲得相同的本地網(wǎng)絡地址到另外的網(wǎng)絡地址(IP地址與類型1的目標IP地址不同)的出口公網(wǎng)地址,并用這個地址與類型1的響應中攜帶的那個出口公網(wǎng)地址進行比較,以此來判斷當前節(jié)點所連接的NAT是否是對稱型的。
除了類型1和類型10之外,發(fā)送其它的消息主要就是看看是否能獲得對應的響應。
根據(jù)發(fā)送的這5種不同類型的消息的響應來判斷當前節(jié)點所連接的NAT的類型并返回給調(diào)用者。
下面我們再用幾張圖來詳細地說明,這些消息都發(fā)到了哪里,而響應又是從哪里返回回來的。
先說明一下,stund的STUN Server需要部署在一臺具有雙網(wǎng)卡且每個網(wǎng)卡都有一個自己公網(wǎng)IP地址的主機上。STUN Server的兩個IP可以稱為IPAddr1(primary IP)和IPAddr2(alt IP),兩個端口可以稱為Port1(primary port)和Port2(alt port),這兩個端口默認分別為3478和3479。STUN Server會打開4個sockets,每個IP兩個分別對應兩個不同的端口。
首先是消息1:
160644_Zta3_919237.png
消息1從客戶端的第一個端口Port1發(fā)向STUN Server的IPAddr1:Port1,響應中則會攜帶客戶端發(fā)送消息的端口的出口網(wǎng)絡地址,及IPAddr2:Port2,以為后續(xù)發(fā)送消息10及消息11做準備。
消息2:
161232_AfFx_919237.png
消息2從客戶端的第二個端口,發(fā)向STUN Server的IPAddr1:Port1,這個消息請求STUN Server將響應從它的IPAddr2:Port1發(fā)送回來,也就是相對于接收數(shù)據(jù)包的網(wǎng)絡地址而言切換一下IP地址的網(wǎng)絡地址。
發(fā)送這個消息的目的是什么呢?這個消息的響應如果能接收到的話,說明當前節(jié)點連接的NAT的類型為全錐型的,說明NAT對于發(fā)向其內(nèi)部的主機的數(shù)據(jù)包幾乎沒有限制。
這里為什么要從第二個端口發(fā)送消息呢?這主要是因為,類型10的消息會發(fā)向IPAddr2:Port1,這實際上會對消息2的響應的接收產(chǎn)生干擾。如果一個地址向IPAddr2:Port1發(fā)送了消息,即使當前節(jié)點連接的NAT的類型不是全錐型的,從IPAddr2:Port1發(fā)回來的消息也可能被接收到。
消息3:
161329_M7lo_919237.png
消息3同樣從客戶端的第二個端口發(fā)出,且同樣發(fā)向STUN Server的IPAddr1:Port1,但這個消息請求STUN Server將響應從它的IPAddr1:Port2發(fā)送回來,也就是相對于接收數(shù)據(jù)包的網(wǎng)絡地址而言切換一下端口的網(wǎng)絡地址。
在消息2的響應接收不到的情況下,如果消息3的響應可以接收到,說明NAT對傳入給內(nèi)部主機的包是限制IP而不限制端口的,也就是說當前節(jié)點連接的NAT的類型是IP限制型的。
消息4:
161413_TSdS_919237.png
針對多主機部署的STUN Server優(yōu)化
由上面的過程,不難看到,STUN Server的部署有一個比較大的限制,即要求部署的主機具有雙網(wǎng)卡,這對于我們當前遍地云主機的環(huán)境而言,部署起來是不那么方便的。主要是對于類型2的消息,客戶端請求STUN Server切換一下IP地址將消息發(fā)回來。
因而一種用于stund的STUN Server的優(yōu)化設計應運而生,結(jié)構(gòu)如下圖:
162407_B2RO_919237.png
這種設計主要是讓STUN Server只綁定一個IP上的兩個端口,同時在STUN之間建立一個通信信道,以便于類型2的消息能得到合適的處理。
針對多主機部署的STUN Server的優(yōu)化當前實現(xiàn)的狀況:
Github主頁:https://github.com/hanpfei/stund
STUN消息的格式
具體可多主機部署的STUN Server要如何設計?這還要從STUN消息的具體格式說起。接著來看下STUN消息的具體格式。
首先是客戶端發(fā)送的請求的格式。我們可以通過stunSendTest()函數(shù)的實現(xiàn)來對這個問題做一番了解:
static void stunSendTest(Socket myFd, StunAddress4& dest, const StunAtrString& username, const StunAtrString& password,int testNum, bool verbose) {assert( dest.addr != 0);assert( dest.port != 0);bool changePort = false;bool changeIP = false;bool discard = false;switch (testNum) {case 1:case 10:case 11:break;case 2://changePort=true;changeIP = true;break;case 3:changePort = true;break;case 4:changeIP = true;break;case 5:discard = true;break;default:cerr << "Test " << testNum << " is unkown\n";assert(0);}StunMessage req;memset(&req, 0, sizeof(StunMessage));stunBuildReqSimple(&req, username, changePort, changeIP, testNum);char buf[STUN_MAX_MESSAGE_SIZE];int len = STUN_MAX_MESSAGE_SIZE;len = stunEncodeMessage(req, buf, len, password, verbose);if (verbose) {clog << "About to send msg of len " << len << " to " << dest << endl;}sendMessage(myFd, buf, len, dest.addr, dest.port, verbose);// add some delay so the packets don't get sent too quickly #ifdef WIN32 // !cj! TODO - should fix this up in windowsclock_t now = clock();assert( CLOCKS_PER_SEC == 1000 );while ( clock() <= now+10 ) {}; #elseusleep(10 * 1000); #endif}從這里似乎也得不到太多STUN消息格式的具體信息,細節(jié)都被放在stunBuildReqSimple()和stunEncodeMessage()兩個函數(shù)中了,接著來看這兩個函數(shù)的實現(xiàn):
static char* encodeAtrChangeRequest(char* ptr, const StunAtrChangeRequest& atr) {ptr = encode16(ptr, ChangeRequest);ptr = encode16(ptr, 4);ptr = encode32(ptr, atr.value);return ptr; }. . . . . .unsigned int stunEncodeMessage(const StunMessage& msg, char* buf, unsigned int bufLen, const StunAtrString& password,bool verbose) {assert(bufLen >= sizeof(StunMsgHdr));char* ptr = buf;ptr = encode16(ptr, msg.msgHdr.msgType);char* lengthp = ptr;ptr = encode16(ptr, 0);ptr = encode(ptr, reinterpret_cast<const char*>(msg.msgHdr.id.octet), sizeof(msg.msgHdr.id));if (verbose)clog << "Encoding stun message: " << endl;if (msg.hasMappedAddress) {if (verbose)clog << "Encoding MappedAddress: " << msg.mappedAddress.ipv4 << endl;ptr = encodeAtrAddress4(ptr, MappedAddress, msg.mappedAddress);}if (msg.hasResponseAddress) {if (verbose)clog << "Encoding ResponseAddress: " << msg.responseAddress.ipv4 << endl;ptr = encodeAtrAddress4(ptr, ResponseAddress, msg.responseAddress);}if (msg.hasChangeRequest) {if (verbose)clog << "Encoding ChangeRequest: " << msg.changeRequest.value << endl;ptr = encodeAtrChangeRequest(ptr, msg.changeRequest);}if (msg.hasSourceAddress) {if (verbose)clog << "Encoding SourceAddress: " << msg.sourceAddress.ipv4 << endl;ptr = encodeAtrAddress4(ptr, SourceAddress, msg.sourceAddress);}if (msg.hasChangedAddress) {if (verbose)clog << "Encoding ChangedAddress: " << msg.changedAddress.ipv4 << endl;ptr = encodeAtrAddress4(ptr, ChangedAddress, msg.changedAddress);}if (msg.hasUsername) {if (verbose)clog << "Encoding Username: " << msg.username.value << endl;ptr = encodeAtrString(ptr, Username, msg.username);}if (msg.hasPassword) {if (verbose)clog << "Encoding Password: " << msg.password.value << endl;ptr = encodeAtrString(ptr, Password, msg.password);}if (msg.hasErrorCode) {if (verbose)clog << "Encoding ErrorCode: class=" << int(msg.errorCode.errorClass) << " number="<< int(msg.errorCode.number) << " reason=" << msg.errorCode.reason << endl;ptr = encodeAtrError(ptr, msg.errorCode);}if (msg.hasUnknownAttributes) {if (verbose)clog << "Encoding UnknownAttribute: ???" << endl;ptr = encodeAtrUnknown(ptr, msg.unknownAttributes);}if (msg.hasReflectedFrom) {if (verbose)clog << "Encoding ReflectedFrom: " << msg.reflectedFrom.ipv4 << endl;ptr = encodeAtrAddress4(ptr, ReflectedFrom, msg.reflectedFrom);}if (msg.hasXorMappedAddress) {if (verbose)clog << "Encoding XorMappedAddress: " << msg.xorMappedAddress.ipv4 << endl;ptr = encodeAtrAddress4(ptr, XorMappedAddress, msg.xorMappedAddress);}if (msg.xorOnly) {if (verbose)clog << "Encoding xorOnly: " << endl;ptr = encodeXorOnly(ptr);}if (msg.hasServerName) {if (verbose)clog << "Encoding ServerName: " << msg.serverName.value << endl;ptr = encodeAtrString(ptr, ServerName, msg.serverName);}if (msg.hasSecondaryAddress) {if (verbose)clog << "Encoding SecondaryAddress: " << msg.secondaryAddress.ipv4 << endl;ptr = encodeAtrAddress4(ptr, SecondaryAddress, msg.secondaryAddress);}if (password.sizeValue > 0) {if (verbose)clog << "HMAC with password: " << password.value << endl;StunAtrIntegrity integrity;computeHmac(integrity.hash, buf, int(ptr - buf), password.value, password.sizeValue);ptr = encodeAtrIntegrity(ptr, integrity);}if (verbose)clog << endl;encode16(lengthp, UInt16(ptr - buf - sizeof(StunMsgHdr)));return int(ptr - buf); }void stunBuildReqSimple(StunMessage* msg, const StunAtrString& username, bool changePort, bool changeIp,unsigned int id) {assert( msg);memset(msg, 0, sizeof(*msg));msg->msgHdr.msgType = BindRequestMsg;for (int i = 0; i < 16; i = i + 4) {assert(i+3<16);int r = stunRand();msg->msgHdr.id.octet[i + 0] = r >> 0;msg->msgHdr.id.octet[i + 1] = r >> 8;msg->msgHdr.id.octet[i + 2] = r >> 16;msg->msgHdr.id.octet[i + 3] = r >> 24;}if (id != 0) {msg->msgHdr.id.octet[0] = id;}msg->hasChangeRequest = true;msg->changeRequest.value = (changeIp ? ChangeIpFlag : 0) | (changePort ? ChangePortFlag : 0);if (username.sizeValue > 0) {msg->hasUsername = true;msg->username = username;} }由這些函數(shù)的實現(xiàn),當不難理出來STUN請求消息的格式大體為:
170658_ZxCq_919237.png
整體來看,STUN請求消息分為兩個部分,一部分是Header,另一部分是Attr的List。
而Header又包含消息的類型,消息不包含Header的長度,及一個128位16字節(jié)的id。在stund中,id的首個字節(jié)保存了消息的類型。STUN Server會原封不動的將客戶端發(fā)過去的消息的id包含在響應中發(fā)回給客戶端,在stund中,使用了id的首個字節(jié)用以區(qū)分發(fā)出去的不同類型的消息的響應。
Attr的List則是一系列的Attr。Attr的結(jié)構(gòu)大體為,先是一個16位的AttrType,然后是16位的Attr值長度,接著便是Attr的值,而Attr的值所占字節(jié)數(shù)因Attr的不同而不同。對于判斷NatType這個case而言,AttrList中只有一個Attr,及類型為ChangeRequest的Attr,它有一個32位4字節(jié)的值。這個Attr用于告訴STUN Server,響應應該從哪個網(wǎng)絡地址發(fā)回來。
看完了STUN請求消息的格式之后,接著再來看STUN響應消息的格式。這個我們可以從stunServerProcessMsg()函數(shù)的實現(xiàn)來了解:
bool stunServerProcessMsg(char* buf, unsigned int bufLen, StunAddress4& from, StunAddress4& secondary,StunAddress4& myAddr, StunAddress4& altAddr, StunMessage* resp, StunAddress4* destination,StunAtrString* hmacPassword, bool* changePort, bool* changeIp, bool verbose) {// set up information for default responsememset(resp, 0, sizeof(*resp));*changeIp = false;*changePort = false;StunMessage req;bool ok = stunParseMessage(buf, bufLen, req, verbose);if (!ok) { // Complete garbage, drop it on the floorif (verbose)clog << "Request did not parse" << endl;return false;}if (verbose)clog << "Request parsed ok" << endl;StunAddress4 mapped = req.mappedAddress.ipv4;StunAddress4 respondTo = req.responseAddress.ipv4;UInt32 flags = req.changeRequest.value;switch (req.msgHdr.msgType) {case SharedSecretRequestMsg:if (verbose)clog << "Received SharedSecretRequestMsg on udp. send error 433." << endl;// !cj! - should fix so you know if this came over TLS or UDPstunCreateSharedSecretResponse(req, from, *resp);//stunCreateSharedSecretErrorResponse(*resp, 4, 33, "this request must be over TLS");return true;case BindRequestMsg:if (!req.hasMessageIntegrity) {if (verbose)clog << "BindRequest does not contain MessageIntegrity" << endl;if (0) { // !jf! mustAuthenticateif (verbose)clog << "Received BindRequest with no MessageIntegrity. Sending 401." << endl;stunCreateErrorResponse(*resp, 4, 1, "Missing MessageIntegrity");return true;}} else {if (!req.hasUsername) {if (verbose)clog << "No UserName. Send 432." << endl;stunCreateErrorResponse(*resp, 4, 32, "No UserName and contains MessageIntegrity");return true;} else {if (verbose)clog << "Validating username: " << req.username.value << endl;// !jf! could retrieve associated password from provisioning hereif (strcmp(req.username.value, "test") == 0) {if (0) {// !jf! if the credentials are stalestunCreateErrorResponse(*resp, 4, 30, "Stale credentials on BindRequest");return true;} else {if (verbose)clog << "Validating MessageIntegrity" << endl;// need access to shared secretunsigned char hmac[20]; #ifndef NOSSLunsigned int hmacSize=20;HMAC(EVP_sha1(),"1234", 4,reinterpret_cast<const unsigned char*>(buf), bufLen-20-4,hmac, &hmacSize);assert(hmacSize == 20); #endifif (memcmp(buf, hmac, 20) != 0) {if (verbose)clog << "MessageIntegrity is bad. Sending " << endl;stunCreateErrorResponse(*resp, 4, 3, "Unknown username. Try test with password 1234");return true;}// need to compute this later after message is filled inresp->hasMessageIntegrity = true;assert(req.hasUsername);resp->hasUsername = true;resp->username = req.username; // copy username in}} else {if (verbose)clog << "Invalid username: " << req.username.value << "Send 430." << endl;}}}// TODO !jf! should check for unknown attributes here and send 420 listing the// unknown attributes.if (respondTo.port == 0)respondTo = from;if (mapped.port == 0)mapped = from;*changeIp = (flags & ChangeIpFlag) ? true : false;*changePort = (flags & ChangePortFlag) ? true : false;if (verbose) {clog << "Request is valid:" << endl;clog << "\t flags=" << flags << endl;clog << "\t changeIp=" << *changeIp << endl;clog << "\t changePort=" << *changePort << endl;clog << "\t from = " << from << endl;clog << "\t respond to = " << respondTo << endl;clog << "\t mapped = " << mapped << endl;}// form the outgoing messageresp->msgHdr.msgType = BindResponseMsg;for (int i = 0; i < 16; i++) {resp->msgHdr.id.octet[i] = req.msgHdr.id.octet[i];}if (req.xorOnly == false) {resp->hasMappedAddress = true;resp->mappedAddress.ipv4.port = mapped.port;resp->mappedAddress.ipv4.addr = mapped.addr;}if (1) { // do xorMapped address or notresp->hasXorMappedAddress = true;UInt16 id16 = req.msgHdr.id.octet[0] << 8 | req.msgHdr.id.octet[1];UInt32 id32 = req.msgHdr.id.octet[0] << 24 | req.msgHdr.id.octet[1] << 16 | req.msgHdr.id.octet[2] << 8| req.msgHdr.id.octet[3];resp->xorMappedAddress.ipv4.port = mapped.port ^ id16;resp->xorMappedAddress.ipv4.addr = mapped.addr ^ id32;}resp->hasSourceAddress = true;resp->sourceAddress.ipv4.port = (*changePort) ? altAddr.port : myAddr.port;resp->sourceAddress.ipv4.addr = (*changeIp) ? altAddr.addr : myAddr.addr;resp->hasChangedAddress = true;resp->changedAddress.ipv4.port = altAddr.port;resp->changedAddress.ipv4.addr = altAddr.addr;if (secondary.port != 0) {resp->hasSecondaryAddress = true;resp->secondaryAddress.ipv4.port = secondary.port;resp->secondaryAddress.ipv4.addr = secondary.addr;}if (req.hasUsername && req.username.sizeValue > 0) {// copy username inresp->hasUsername = true;assert( req.username.sizeValue % 4 == 0);assert( req.username.sizeValue < STUN_MAX_STRING);memcpy(resp->username.value, req.username.value, req.username.sizeValue);resp->username.sizeValue = req.username.sizeValue;}if (1) { // add ServerNameresp->hasServerName = true;const char serverName[] = "Vovida.org " STUN_VERSION; // must pad to mult of 4assert( sizeof(serverName) < STUN_MAX_STRING);//cerr << "sizeof serverName is " << sizeof(serverName) << endl;assert( sizeof(serverName)%4 == 0);memcpy(resp->serverName.value, serverName, sizeof(serverName));resp->serverName.sizeValue = sizeof(serverName);}if (req.hasMessageIntegrity & req.hasUsername) {// this creates the password that will be used in the HMAC when then// messages is sentstunCreatePassword(req.username, hmacPassword);}if (req.hasUsername && (req.username.sizeValue > 64)) {UInt32 source;assert( sizeof(int) == sizeof(UInt32));sscanf(req.username.value, "%x", &source);resp->hasReflectedFrom = true;resp->reflectedFrom.ipv4.port = 0;resp->reflectedFrom.ipv4.addr = source;}destination->port = respondTo.port;destination->addr = respondTo.addr;return true;default:if (verbose)clog << "Unknown or unsupported request " << endl;return false;}assert(0);return false; }由這個函數(shù)的實現(xiàn),我們不難看出STUN Server發(fā)回給客戶端的響應的消息格式與請求的格式大體一樣,但消息的具體內(nèi)容有一些區(qū)別。消息的格式大體為:
174120_pPyU_919237.png
這個消息里的內(nèi)容要多一點。
了解了STUN客戶端和STUN Server間交互的這些UDP數(shù)據(jù)包的格式之后,我們就可以確定可雙主機部署的STUN Server間通信的消息的格式了。
仔細來看stunServerProcessMsg(),我們注意到,STUN server響應發(fā)送的目標地址,以及返回給客戶端的它的出口公網(wǎng)地址也就是mappedAddress也沒有限定只能是from地址,這些值也可以來源于請求消息。
借助于stund的這些良好設計,可以大大簡化我們的可雙主機部署的STUN server的設計與實現(xiàn)。STUN server間的消息格式可以為:
182922_6Bzw_919237.png
也就是說,當STUN Server收到類型2的消息時,構(gòu)造一個格式如上圖的消息,并將該消息轉(zhuǎn)發(fā)給另為一個STUN Server。其中MappedAddress和ResponseAddress Attr的值都是消息的from地址,即客戶端發(fā)送消息的端口的出口公網(wǎng)地址。
經(jīng)過對stunServerProcessMsg()的一番改造,終于可以實現(xiàn)STUN Server的多主機部署,其改造后的實現(xiàn)為:
bool stunServerProcessMsg(StunServerInfo& info, char* buf, unsigned int bufLen, StunAddress4& from,StunAddress4& secondary, StunAddress4& myAddr, StunAddress4& altAddr, StunMessage* resp,StunAddress4* destination, StunAtrString* hmacPassword, bool* changePort, bool* changeIp,bool verbose) {// set up information for default responsememset(resp, 0, sizeof(*resp));*changeIp = false;*changePort = false;StunMessage req;bool ok = stunParseMessage(buf, bufLen, req, verbose);if (!ok) { // Complete garbage, drop it on the floorif (verbose)clog << "Request did not parse" << endl;return false;}if (verbose)clog << "Request parsed ok" << endl;StunAddress4 mapped = req.mappedAddress.ipv4;StunAddress4 respondTo = req.responseAddress.ipv4;UInt32 flags = req.changeRequest.value;switch (req.msgHdr.msgType) {case SharedSecretRequestMsg:if (verbose)clog << "Received SharedSecretRequestMsg on udp. send error 433." << endl;// !cj! - should fix so you know if this came over TLS or UDPstunCreateSharedSecretResponse(req, from, *resp);//stunCreateSharedSecretErrorResponse(*resp, 4, 33, "this request must be over TLS");return true;case BindRequestMsg:if (!req.hasMessageIntegrity) {if (verbose)clog << "BindRequest does not contain MessageIntegrity" << endl;if (0) { // !jf! mustAuthenticateif (verbose)clog << "Received BindRequest with no MessageIntegrity. Sending 401." << endl;stunCreateErrorResponse(*resp, 4, 1, "Missing MessageIntegrity");return true;}} else {if (!req.hasUsername) {if (verbose)clog << "No UserName. Send 432." << endl;stunCreateErrorResponse(*resp, 4, 32, "No UserName and contains MessageIntegrity");return true;} else {if (verbose)clog << "Validating username: " << req.username.value << endl;// !jf! could retrieve associated password from provisioning hereif (strcmp(req.username.value, "test") == 0) {if (0) {// !jf! if the credentials are stalestunCreateErrorResponse(*resp, 4, 30, "Stale credentials on BindRequest");return true;} else {if (verbose)clog << "Validating MessageIntegrity" << endl;// need access to shared secretunsigned char hmac[20]; #ifndef NOSSLunsigned int hmacSize=20;HMAC(EVP_sha1(),"1234", 4,reinterpret_cast<const unsigned char*>(buf), bufLen-20-4,hmac, &hmacSize);assert(hmacSize == 20); #endifif (memcmp(buf, hmac, 20) != 0) {if (verbose)clog << "MessageIntegrity is bad. Sending " << endl;stunCreateErrorResponse(*resp, 4, 3, "Unknown username. Try test with password 1234");return true;}// need to compute this later after message is filled inresp->hasMessageIntegrity = true;assert(req.hasUsername);resp->hasUsername = true;resp->username = req.username; // copy username in}} else {if (verbose)clog << "Invalid username: " << req.username.value << "Send 430." << endl;}}}// TODO !jf! should check for unknown attributes here and send 420 listing the// unknown attributes.if (respondTo.port == 0)respondTo = from;if (mapped.port == 0)mapped = from;*changeIp = (flags & ChangeIpFlag) ? true : false;*changePort = (flags & ChangePortFlag) ? true : false;if (verbose) {clog << "Request is valid:" << endl;clog << "\t flags=" << flags << endl;clog << "\t changeIp=" << *changeIp << endl;clog << "\t changePort=" << *changePort << endl;clog << "\t from = " << from << endl;clog << "\t respond to = " << respondTo << endl;clog << "\t mapped = " << mapped << endl;}// form the outgoing messagefor (int i = 0; i < 16; i++) {resp->msgHdr.id.octet[i] = req.msgHdr.id.octet[i];}if (*changeIp && info.altIpFd == INVALID_SOCKET) {resp->msgHdr.msgType = req.msgHdr.msgType;*changeIp = false;*changePort = false;resp->hasChangeRequest = true;resp->changeRequest.value = changePort ? ChangePortFlag : 0;resp->hasMappedAddress = true;resp->mappedAddress.ipv4.port = mapped.port;resp->mappedAddress.ipv4.addr = mapped.addr;resp->hasResponseAddress = true;resp->responseAddress.ipv4.port = from.port;resp->responseAddress.ipv4.addr = from.addr;respondTo.port = info.myAddr.port;respondTo.addr = info.altAddr.addr;if (verbose) {clog << "\t respondTo change = " << respondTo << endl;}} else {resp->msgHdr.msgType = BindResponseMsg;if (req.xorOnly == false) {resp->hasMappedAddress = true;resp->mappedAddress.ipv4.port = mapped.port;resp->mappedAddress.ipv4.addr = mapped.addr;}if (1) { // do xorMapped address or notresp->hasXorMappedAddress = true;UInt16 id16 = req.msgHdr.id.octet[0] << 8 | req.msgHdr.id.octet[1];UInt32 id32 = req.msgHdr.id.octet[0] << 24 | req.msgHdr.id.octet[1] << 16| req.msgHdr.id.octet[2] << 8 | req.msgHdr.id.octet[3];resp->xorMappedAddress.ipv4.port = mapped.port ^ id16;resp->xorMappedAddress.ipv4.addr = mapped.addr ^ id32;}resp->hasSourceAddress = true;resp->sourceAddress.ipv4.port = (*changePort) ? altAddr.port : myAddr.port;resp->sourceAddress.ipv4.addr = (*changeIp) ? altAddr.addr : myAddr.addr;resp->hasChangedAddress = true;resp->changedAddress.ipv4.port = altAddr.port;resp->changedAddress.ipv4.addr = altAddr.addr;if (secondary.port != 0) {resp->hasSecondaryAddress = true;resp->secondaryAddress.ipv4.port = secondary.port;resp->secondaryAddress.ipv4.addr = secondary.addr;}if (req.hasUsername && req.username.sizeValue > 0) {// copy username inresp->hasUsername = true;assert( req.username.sizeValue % 4 == 0);assert( req.username.sizeValue < STUN_MAX_STRING);memcpy(resp->username.value, req.username.value, req.username.sizeValue);resp->username.sizeValue = req.username.sizeValue;}if (1) { // add ServerNameresp->hasServerName = true;const char serverName[] = "Vovida.org " STUN_VERSION; // must pad to mult of 4assert( sizeof(serverName) < STUN_MAX_STRING);//cerr << "sizeof serverName is " << sizeof(serverName) << endl;assert( sizeof(serverName)%4 == 0);memcpy(resp->serverName.value, serverName, sizeof(serverName));resp->serverName.sizeValue = sizeof(serverName);}if (req.hasMessageIntegrity & req.hasUsername) {// this creates the password that will be used in the HMAC when then// messages is sentstunCreatePassword(req.username, hmacPassword);}if (req.hasUsername && (req.username.sizeValue > 64)) {UInt32 source;assert( sizeof(int) == sizeof(UInt32));sscanf(req.username.value, "%x", &source);resp->hasReflectedFrom = true;resp->reflectedFrom.ipv4.port = 0;resp->reflectedFrom.ipv4.addr = source;}}destination->port = respondTo.port;destination->addr = respondTo.addr;return true;default:if (verbose)clog << "Unknown or unsupported request " << endl;return false;}assert(0);return false; }主要的改動即是在發(fā)現(xiàn)客戶端請求改變IP地址發(fā)回響應時,構(gòu)造如上圖中的消息,并發(fā)給另一個STUN Server。從而,對于消息2,數(shù)據(jù)包的流轉(zhuǎn)過程大體如下:
140802_Enu2_919237.png
Done。
總結(jié)
以上是生活随笔為你收集整理的标准STUN判断NAT类型的过程及改进的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OkHttp实现分析之Websocket
- 下一篇: WiFi 热点共享设置