Derek解讀Bytom原始碼-P2P網路 upnp埠對映

比原鏈Bytom發表於2018-08-28

作者:Derek

簡介

Github地址:https://github.com/Bytom/bytom

Gitee地址:https://gitee.com/BytomBlockc…

本章介紹bytom程式碼P2P網路中upnp埠對映

作者使用MacOS作業系統,其他平臺也大同小異

Golang Version: 1.8

UPNP介紹

UPNP(Universal Plug and Play)通用即插即用。UPNP埠對映將一個外部埠對映到一個內網ip:port。從而實現p2p網路從外網能夠穿透閘道器訪問到內網的bytomd節點。

UPNP協議

SSDP(Simple Service Discovery Protocol 簡單服務發現協議)
GENA(Generic Event Notification Architecture 通用事件通知結構)
SOAP(Simple Object Access Protocol 簡單物件訪問協議)
XML(Extensible Markup Language 可擴張標記語言)

UPNP程式碼

p2p/upnp/upnp.go

發現網路中支援UPNP功能的裝置

從網路中發現支援UPNP功能的裝置,並得到該裝置的location和url等相關資訊

type upnpNAT struct {
    serviceURL string // 裝置的描述檔案URL,用於得到該裝置的描述資訊
    ourIP      string // 節點本地ip地址
    urnDomain  string // 裝置型別
}

func Discover() (nat NAT, err error) {
    ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900")
    if err != nil {
        return
    }
    conn, err := net.ListenPacket("udp4", ":0")
    if err != nil {
        return
    }
    socket := conn.(*net.UDPConn)
    defer socket.Close()

    err = socket.SetDeadline(time.Now().Add(3 * time.Second))
    if err != nil {
        return
    }

    st := "InternetGatewayDevice:1"

    // 多播請求:M-SEARCH SSDP協議定義的發現請求。
    buf := bytes.NewBufferString(
        "M-SEARCH * HTTP/1.1
" +
            "HOST: 239.255.255.250:1900
" +
            "ST: ssdp:all
" +
            "MAN: "ssdp:discover"
" +
            "MX: 2

")
    message := buf.Bytes()
    answerBytes := make([]byte, 1024)
    for i := 0; i < 3; i++ {
        // 向239.255.255.250:1900傳送一條多播請求
        _, err = socket.WriteToUDP(message, ssdp)
        if err != nil {
            return
        }
        // 如果從網路中發現UPNP裝置則會從239.255.255.250:1900收到響應訊息
        var n int
        n, _, err = socket.ReadFromUDP(answerBytes)
        for {
            n, _, err = socket.ReadFromUDP(answerBytes)
            if err != nil {
                break
            }
            answer := string(answerBytes[0:n])
            if strings.Index(answer, st) < 0 {
                continue
            }
            // HTTP header field names are case-insensitive.
            // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
            // 獲得裝置location
            locString := "
location:"
            answer = strings.ToLower(answer)
            locIndex := strings.Index(answer, locString)
            if locIndex < 0 {
                continue
            }
            loc := answer[locIndex+len(locString):]
            endIndex := strings.Index(loc, "
")
            if endIndex < 0 {
                continue
            }
            // 獲得裝置的描述url和裝置型別
            locURL := strings.TrimSpace(loc[0:endIndex])
            var serviceURL, urnDomain string
            serviceURL, urnDomain, err = getServiceURL(locURL)
            if err != nil {
                return
            }
            var ourIP net.IP
            ourIP, err = localIPv4()
            if err != nil {
                return
            }
            nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP.String(), urnDomain: urnDomain}
            return
        }
    }
    err = errors.New("UPnP port discovery failed.")
    return
}

新增埠對映

向upnp裝置傳送一條http post請求,將內部網路ip:port和外部網路ip:port做對映

func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) {
    // A single concatenation would break ARM compilation.
    message := "<u:AddPortMapping xmlns:u="urn:" + n.urnDomain + ":service:WANIPConnection:1">
" +
        "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort)
    message += "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>"
    message += "<NewInternalPort>" + strconv.Itoa(internalPort) + "</NewInternalPort>" +
        "<NewInternalClient>" + n.ourIP + "</NewInternalClient>" +
        "<NewEnabled>1</NewEnabled><NewPortMappingDescription>"
    message += description +
        "</NewPortMappingDescription><NewLeaseDuration>" + strconv.Itoa(timeout) +
        "</NewLeaseDuration></u:AddPortMapping>"

    var response *http.Response
    response, err = soapRequest(n.serviceURL, "AddPortMapping", message, n.urnDomain)
    if response != nil {
        defer response.Body.Close()
    }
    if err != nil {
        return
    }

    // TODO: check response to see if the port was forwarded
    // log.Println(message, response)
    // JAE:
    // body, err := ioutil.ReadAll(response.Body)
    // fmt.Println(string(body), err)
    mappedExternalPort = externalPort
    _ = response
    return
}

刪除埠對映

向upnp裝置傳送一條http post請求,將內部網路ip:port和外部網路ip:port刪除對映關係

func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) {

    message := "<u:DeletePortMapping xmlns:u="urn:" + n.urnDomain + ":service:WANIPConnection:1">
" +
        "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) +
        "</NewExternalPort><NewProtocol>" + protocol + "</NewProtocol>" +
        "</u:DeletePortMapping>"

    var response *http.Response
    response, err = soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain)
    if response != nil {
        defer response.Body.Close()
    }
    if err != nil {
        return
    }

    // TODO: check response to see if the port was deleted
    // log.Println(message, response)
    _ = response
    return
}

獲取對映後的公網地址

func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) {
    info, err := n.getExternalIPAddress()
    if err != nil {
        return
    }
    addr = net.ParseIP(info.externalIpAddress)
    return
}

func (n *upnpNAT) getExternalIPAddress() (info statusInfo, err error) {

    message := "<u:GetExternalIPAddress xmlns:u="urn:" + n.urnDomain + ":service:WANIPConnection:1">
" +
        "</u:GetExternalIPAddress>"

    var response *http.Response
    response, err = soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain)
    if response != nil {
        defer response.Body.Close()
    }
    if err != nil {
        return
    }
    var envelope Envelope
    data, err := ioutil.ReadAll(response.Body)
    reader := bytes.NewReader(data)
    xml.NewDecoder(reader).Decode(&envelope)

    info = statusInfo{envelope.Soap.ExternalIP.IPAddress}

    if err != nil {
        return
    }

    return
}

相關文章