作者:Derek

简介

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

Gitee地址:https://gitee.com/BytomBlockchain/bytom

本章介绍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 Access Protocol 简单对象访问协议)
(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.1rn\" +
            \"HOST: 239.255.255.250:1900rn\" +
            \"ST: ssdp:allrn\" +
            \"MAN: \"ssdp:discover\"rn\" +
            \"MX: 2rnrn\")
    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 := \"rnlocation:\"
            answer = strings.ToLower(answer)
            locIndex := strings.Index(answer, locString)
            if locIndex < 0 {
                continue
            }
            loc := answer[locIndex+len(locString):]
            endIndex := strings.Index(loc, \"rn\")
            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, de ion string, timeout int) (mappedExternalPort int, err error) {
    // A single concatenation would break ARM compilation.
    message := \"<u:AddPortMapping  ns:u=\"urn:\" + n.urnDomain + \":service:WANIPConnection:1\">rn\" +
        \"<NewRemoteHost></NewRemoteHost><NewExternalPort>\" + strconv.Itoa(externalPort)
    message += \"</NewExternalPort><NewProtocol>\" + protocol + \"</NewProtocol>\"
    message += \"<NewInternalPort>\" + strconv.Itoa(internalPort) + \"</NewInternalPort>\" +
        \"<NewInternalClient>\" + n.ourIP + \"</NewInternalClient>\" +
        \"<NewEnabled>1</NewEnabled><NewPortMappingDe ion>\"
    message += de ion +
        \"</NewPortMappingDe ion><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  ns:u=\"urn:\" + n.urnDomain + \":service:WANIPConnection:1\">rn\" +
        \"<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  ns:u=\"urn:\" + n.urnDomain + \":service:WANIPConnection:1\">rn\" +
        \"</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)
     .NewDecoder(reader).Decode(&envelope)

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

    if err != nil {
        return
    }

    return
}
收藏 打印