這裏基於stund的實現,來研究標準STUN協議,判斷NatType的過程。 c++
首先來看stund中用於判斷NatType的接口的用法。這裏主要來看stund中的STUN客戶端client.cxx的實現。client.cxx是一個常規的C/C++ app,這個app的主要code以下: git
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 worked testNum = 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 default numNic = 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"; else cout << ", random port"; if (hairpin) cout << ", will hairpin"; else cout << ", no hairpin"; retval[nic] = 0x02; break; case StunTypeDependentFilter: cout << "Independent Mapping, Address Dependent Filter"; if (presPort) cout << ", preserves ports"; else cout << ", random port"; if (hairpin) cout << ", will hairpin"; else cout << ", no hairpin"; retval[nic] = 0x04; break; case StunTypePortDependedFilter: cout << "Independent Mapping, Port Dependent Filter"; if (presPort) cout << ", preserves ports"; else cout << ", random port"; if (hairpin) cout << ", will hairpin"; else cout << ", no hairpin"; retval[nic] = 0x06; break; case StunTypeDependentMapping: cout << "Dependent Mapping"; if (presPort) cout << ", preserves ports"; else cout << ", random port"; if (hairpin) cout << ", will hairpin"; else cout << ", 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 type break; } cout << "\t"; cout.flush(); if (!hairpin) { retval[nic] |= 0x10; } if (presPort) { retval[nic] |= 0x01; } } else if (testNum == 100) {
能夠看到這個app主要作了3件事情: github
1. 解析參數。主要從參數中得到STUN server的地址,及本地用於發送數據包所用的UDP端口號。 windows
2. 調用stunNatType()函數判斷NatType。判斷NatType的所有邏輯都在這個函數裏。 網絡
3. 將stunNatType()函數返回的NatType進行格式化並打印輸出,以便於人的閱讀。 併發
接着來看stunNatType()函數的實現 app
stunNatType()函數的實現以下:
dom
NatType stunNatType(StunAddress4& dest, bool verbose, bool* preservePort, // if set, is return for if NAT preservers ports or not bool* hairpin, // if set, is the return for if NAT will hairpin packets int port, // port to use for the test, 0 to choose random port StunAddress4* 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 ); #endif int count = 0; while (count < 7) { struct timeval tv; fd_set fdSet; #ifdef WIN32 unsigned int fdSetSize; #else int fdSetSize; #endif FD_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 ms if (count == 0) tv.tv_usec = 0; int err = select(fdSetSize, &fdSet, NULL, NULL, &tv); int e = getErrno(); if (err == SOCKET_ERROR) { // error occured cerr << "Error " << e << " " << strerror(e) << " in select" << endl; return StunTypeFailure; } else if (err == 0) { // timeout occured count++; if (!respTestI) { stunSendTest(myFd1, dest, username, password, 1, verbose); } if ((!respTestI2) && respTestI) { // check the address to send to if valid if ((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 fd for (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 RFC if (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; } #else if (respTestI) { // not blocked if (isNat) { if (mappedIpSame) { if (respTestII) { return StunTypeIndependentFilter; } else { if (respTestIII) { return StunTypeDependentFilter; } else { return StunTypePortDependedFilter; } } } else { // mappedIp is not same return StunTypeDependentMapping; } } else { // isNat is false if (respTestII) { return StunTypeOpen; } else { return StunTypeFirewall; } } } else { return StunTypeBlocked; } #endif return StunTypeUnknown; }
能夠看到這個函數主要作了幾件事: socket
1. 打開了兩個UDP socket。後續會經過這兩個socket來進行數據包的發送,並最終根據這些數據包的響應數據包的狀況來判斷NatType。 函數
2. 向STUN server發送請求。調用stunSendTest()函數發送了5種不一樣類型的消息,各個消息之間的差別也僅僅在與stunSendTest()函數的testNum參數不一樣。這裏咱們也用testNum來區分不一樣的消息,咱們稱它們分別爲類型1,類型2,類型3,類型10及類型11的消息。
其中類型10和類型11的消息依賴於類型1的消息的響應,但類型2和類型3的消息的發送則與類型1的消息的發送及響應相互獨立,於是它們能夠與類型1的消息並行的發送。
3. 接收發送的消息的響應。
從類型1的消息的響應中得到的東西比較多。類型10和類型11的消息要發送的目標地址,都來源於類型1的消息的響應。
類型10的消息發向類型1的消息的響應的changedAddress地址。這個地址是STUN server的副IP地址及端口號。
類型11的消息則發向類型1的消息的響應的testImappedAddr地址,這個地址是發送消息的地址的出口公網地址,向這個消息發送消息實際是向本節點在發送消息,這麼作的實際目的是爲了測試節點所鏈接的NAT是否支持消息的回傳,或者說測試NAT是不是hairpin的。即若是這個類型11的消息經過NAT並最終被髮送給本節點且本節點接收到了這個消息,則說明本節點所鏈接的NAT是hairpin的。
STUN終端會從類型10的消息的響應中得到相同的本地網絡地址到另外的網絡地址(IP地址與類型1的目標IP地址不一樣)的出口公網地址,並用這個地址與類型1的響應中攜帶的那個出口公網地址進行比較,以此來判斷當前節點所鏈接的NAT是不是對稱型的。
除了類型1和類型10以外,發送其它的消息主要就是看看是否能得到對應的響應。
4. 根據發送的這5種不一樣類型的消息的響應來判斷當前節點所鏈接的NAT的類型並返回給調用者。
下面咱們再用幾張圖來詳細地說明,這些消息都發到了哪裏,而響應又是從哪裏返回回來的。
先說明一下,stund的STUN Server須要部署在一臺具備雙網卡且每一個網卡都有一個本身公網IP地址的主機上。STUN Server的兩個IP能夠稱爲IPAddr1(primary IP)和IPAddr2(alt IP),兩個端口能夠稱爲Port1(primary port)和Port2(alt port),這兩個端口默認分別爲3478和3479。STUN Server會打開4個sockets,每一個IP兩個分別對應兩個不一樣的端口。
首先是消息1:
消息1從客戶端的第一個端口Port1發向STUN Server的IPAddr1:Port1,響應中則會攜帶客戶端發送消息的端口的出口網絡地址,及IPAddr2:Port2,覺得後續發送消息10及消息11作準備。
消息2:
消息2從客戶端的第二個端口,發向STUN Server的IPAddr1:Port1,這個消息請求STUN Server將響應從它的IPAddr2:Port1發送回來,也就是相對於接收數據包的網絡地址而言切換一下IP地址的網絡地址。
發送這個消息的目的是什麼呢?這個消息的響應若是能接收到的話,說明當前節點鏈接的NAT的類型爲全錐型的,說明NAT對於發向其內部的主機的數據包幾乎沒有限制。
這裏爲何要從第二個端口發送消息呢?這主要是由於,類型10的消息會發向IPAddr2:Port1,這實際上會對消息2的響應的接收產生干擾。若是一個地址向IPAddr2:Port1發送了消息,即便當前節點鏈接的NAT的類型不是全錐型的,從IPAddr2:Port1發回來的消息也可能被接收到。
消息3:
消息3一樣從客戶端的第二個端口發出,且一樣發向STUN Server的IPAddr1:Port1,但這個消息請求STUN Server將響應從它的IPAddr1:Port2發送回來,也就是相對於接收數據包的網絡地址而言切換一下端口的網絡地址。
在消息2的響應接收不到的狀況下,若是消息3的響應能夠接收到,說明NAT對傳入給內部主機的包是限制IP而不限制端口的,也就是說當前節點鏈接的NAT的類型是IP限制型的。
消息4:
消息4從客戶端的Port1發向STUN Server的IPAddr2:Port1。這個消息主要用來判斷終端相同的內部端口發向不一樣的目標主機時,其出口公網地址是否相同。
由上面的過程,不難看到,STUN Server的部署有一個比較大的限制,即要求部署的主機具備雙網卡,這對於咱們當前遍地雲主機的環境而言,部署起來是不那麼方便的。主要是對於類型2的消息,客戶端請求STUN Server切換一下IP地址將消息發回來。
於是一種用於stund的STUN Server的優化設計應運而生,結構以下圖:
這種設計主要是讓STUN Server只綁定一個IP上的兩個端口,同時在STUN之間創建一個通訊信道,以便於類型2的消息能獲得合適的處理。
針對多主機部署的STUN Server的優化當前實現的情況:
Github主頁:https://github.com/hanpfei/stund
具體可多主機部署的STUN Server要如何設計?這還要從STUN消息的具體格式提及。接着來看下STUN消息的具體格式。
首先是客戶端發送的請求的格式。咱們能夠經過stunSendTest()函數的實現來對這個問題作一番瞭解:
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 windows clock_t now = clock(); assert( CLOCKS_PER_SEC == 1000 ); while ( clock() <= now+10 ) {}; #else usleep(10 * 1000); #endif }
從這裏彷佛也得不到太多STUN消息格式的具體信息,細節都被放在stunBuildReqSimple()和stunEncodeMessage()兩個函數中了,接着來看這兩個函數的實現:
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; } }
由這些函數的實現,當不難理出來STUN請求消息的格式大致爲:
總體來看,STUN請求消息分爲兩個部分,一部分是Header,另外一部分是Attr的List。
而Header又包含消息的類型,消息不包含Header的長度,及一個128位16字節的id。在stund中,id的首個字節保存了消息的類型。STUN Server會原封不動的將客戶端發過去的消息的id包含在響應中發回給客戶端,在stund中,使用了id的首個字節用以區分發出去的不一樣類型的消息的響應。
Attr的List則是一系列的Attr。Attr的結構大致爲,先是一個16位的AttrType,而後是16位的Attr值長度,接着即是Attr的值,而Attr的值所佔字節數因Attr的不一樣而不一樣。對於判斷NatType這個case而言,AttrList中只有一個Attr,及類型爲ChangeRequest的Attr,它有一個32位4字節的值。這個Attr用於告訴STUN Server,響應應該從哪一個網絡地址發回來。
看完了STUN請求消息的格式以後,接着再來看STUN響應消息的格式。這個咱們能夠從stunServerProcessMsg()函數的實現來了解:
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 response memset(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 floor if (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 UDP stunCreateSharedSecretResponse(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! mustAuthenticate if (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 here if (strcmp(req.username.value, "test") == 0) { if (0) { // !jf! if the credentials are stale stunCreateErrorResponse(*resp, 4, 30, "Stale credentials on BindRequest"); return true; } else { if (verbose) clog << "Validating MessageIntegrity" << endl; // need access to shared secret unsigned char hmac[20]; #ifndef NOSSL unsigned int hmacSize=20; HMAC(EVP_sha1(), "1234", 4, reinterpret_cast<const unsigned char*>(buf), bufLen-20-4, hmac, &hmacSize); assert(hmacSize == 20); #endif if (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 in resp->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 message resp->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 not resp->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 in resp->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 ServerName resp->hasServerName = true; const char serverName[] = "Vovida.org " STUN_VERSION; // must pad to mult of 4 assert( 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 sent stunCreatePassword(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; }
由這個函數的實現,咱們不難看出STUN Server發回給客戶端的響應的消息格式與請求的格式大致同樣,但消息的具體內容有一些區別。消息的格式大致爲:
這個消息裏的內容要多一點。
瞭解了STUN客戶端和STUN Server間交互的這些UDP數據包的格式以後,咱們就能夠肯定可雙主機部署的STUN Server間通訊的消息的格式了。
仔細來看stunServerProcessMsg(),咱們注意到,STUN server響應發送的目標地址,以及返回給客戶端的它的出口公網地址也就是mappedAddress也沒有限定只能是from地址,這些值也能夠來源於請求消息。
藉助於stund的這些良好設計,能夠大大簡化咱們的可雙主機部署的STUN server的設計與實現。STUN server間的消息格式能夠爲:
也就是說,當STUN Server收到類型2的消息時,構造一個格式如上圖的消息,並將該消息轉發給另爲一個STUN Server。其中MappedAddress和ResponseAddress Attr的值都是消息的from地址,即客戶端發送消息的端口的出口公網地址。
通過對stunServerProcessMsg()的一番改造,終於能夠實現STUN Server的多主機部署,其改造後的實現爲:
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 response memset(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 floor if (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 UDP stunCreateSharedSecretResponse(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! mustAuthenticate if (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 here if (strcmp(req.username.value, "test") == 0) { if (0) { // !jf! if the credentials are stale stunCreateErrorResponse(*resp, 4, 30, "Stale credentials on BindRequest"); return true; } else { if (verbose) clog << "Validating MessageIntegrity" << endl; // need access to shared secret unsigned char hmac[20]; #ifndef NOSSL unsigned int hmacSize=20; HMAC(EVP_sha1(), "1234", 4, reinterpret_cast<const unsigned char*>(buf), bufLen-20-4, hmac, &hmacSize); assert(hmacSize == 20); #endif if (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 in resp->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 message for (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 not resp->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 in resp->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 ServerName resp->hasServerName = true; const char serverName[] = "Vovida.org " STUN_VERSION; // must pad to mult of 4 assert( 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 sent stunCreatePassword(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; }
主要的改動便是在發現客戶端請求改變IP地址發回響應時,構造如上圖中的消息,併發給另外一個STUN Server。從而,對於消息2,數據包的流轉過程大致以下:
Done。