美文网首页多媒体开发
标准STUN判断NAT类型的过程及改进

标准STUN判断NAT类型的过程及改进

作者: hanpfei | 来源:发表于2017-05-31 10:05 被阅读925次

    这里基于stund的实现,来研究标准STUN协议,判断NatType的过程。

    stund用于判断NatType的接口的用法

    首先来看stund中用于判断NatType的接口的用法。这里主要来看stund中的STUN客户端client.cxx的实现。client.cxx是一个常规的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 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件事情:

    1. 解析参数。主要从参数中获得STUN server的地址,及本地用于发送数据包所用的UDP端口号。

    2. 调用stunNatType()函数判断NatType。判断NatType的全部逻辑都在这个函数里。

    3. 将stunNatType()函数返回的NatType进行格式化并打印输出,以便于人的阅读。

    接着来看stunNatType()函数的实现

    stunNatType()函数的实现

    stunNatType()函数的实现如下:

    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;
    }
    

    可以看到这个函数主要做了几件事:

    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:


    160644_Zta3_919237.png

    消息1从客户端的第一个端口Port1发向STUN Server的IPAddr1:Port1,响应中则会携带客户端发送消息的端口的出口网络地址,及IPAddr2:Port2,以为后续发送消息10及消息11做准备。

    消息2:

    161232_AfFx_919237.png

    消息2从客户端的第二个端口,发向STUN Server的IPAddr1:Port1,这个消息请求STUN Server将响应从它的IPAddr2:Port1发送回来,也就是相对于接收数据包的网络地址而言切换一下IP地址的网络地址。

    发送这个消息的目的是什么呢?这个消息的响应如果能接收到的话,说明当前节点连接的NAT的类型为全锥型的,说明NAT对于发向其内部的主机的数据包几乎没有限制。

    这里为什么要从第二个端口发送消息呢?这主要是因为,类型10的消息会发向IPAddr2:Port1,这实际上会对消息2的响应的接收产生干扰。如果一个地址向IPAddr2:Port1发送了消息,即使当前节点连接的NAT的类型不是全锥型的,从IPAddr2:Port1发回来的消息也可能被接收到。

    消息3:


    161329_M7lo_919237.png

    消息3同样从客户端的第二个端口发出,且同样发向STUN Server的IPAddr1:Port1,但这个消息请求STUN Server将响应从它的IPAddr1:Port2发送回来,也就是相对于接收数据包的网络地址而言切换一下端口的网络地址。

    在消息2的响应接收不到的情况下,如果消息3的响应可以接收到,说明NAT对传入给内部主机的包是限制IP而不限制端口的,也就是说当前节点连接的NAT的类型是IP限制型的。

    消息4:

    161413_TSdS_919237.png

    针对多主机部署的STUN Server优化

    由上面的过程,不难看到,STUN Server的部署有一个比较大的限制,即要求部署的主机具有双网卡,这对于我们当前遍地云主机的环境而言,部署起来是不那么方便的。主要是对于类型2的消息,客户端请求STUN Server切换一下IP地址将消息发回来。

    因而一种用于stund的STUN Server的优化设计应运而生,结构如下图:

    162407_B2RO_919237.png

    这种设计主要是让STUN Server只绑定一个IP上的两个端口,同时在STUN之间建立一个通信信道,以便于类型2的消息能得到合适的处理。

    针对多主机部署的STUN Server的优化当前实现的状况:
    Github主页:https://github.com/hanpfei/stund

    STUN消息的格式

    具体可多主机部署的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请求消息的格式大体为:


    170658_ZxCq_919237.png

    整体来看,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发回给客户端的响应的消息格式与请求的格式大体一样,但消息的具体内容有一些区别。消息的格式大体为:

    174120_pPyU_919237.png

    这个消息里的内容要多一点。

    了解了STUN客户端和STUN Server间交互的这些UDP数据包的格式之后,我们就可以确定可双主机部署的STUN Server间通信的消息的格式了。

    仔细来看stunServerProcessMsg(),我们注意到,STUN server响应发送的目标地址,以及返回给客户端的它的出口公网地址也就是mappedAddress也没有限定只能是from地址,这些值也可以来源于请求消息。

    借助于stund的这些良好设计,可以大大简化我们的可双主机部署的STUN server的设计与实现。STUN server间的消息格式可以为:

    182922_6Bzw_919237.png

    也就是说,当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,数据包的流转过程大体如下:

    140802_Enu2_919237.png

    Done。

    相关文章

      网友评论

        本文标题:标准STUN判断NAT类型的过程及改进

        本文链接:https://www.haomeiwen.com/subject/xilrfxtx.html