Derek解读Bytom源码-P2P网络 upnp端口映射

作者: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 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\r\n" +
            "HOST: 239.255.255.250:1900\r\n" +
            "ST: ssdp:all\r\n" +
            "MAN: \"ssdp:discover\"\r\n" +
            "MX: 2\r\n\r\n")
    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 := "\r\nlocation:"
            answer = strings.ToLower(answer)
            locIndex := strings.Index(answer, locString)
            if locIndex < 0 {
                continue
            }
            loc := answer[locIndex+len(locString):]
            endIndex := strings.Index(loc, "\r\n")
            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\">\r\n" +
        "<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\">\r\n" +
        "<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\">\r\n" +
        "</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
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Linux驱动

16.Linux-LCD驱动(详解)

在上一节LCD层次分析中,得出写个LCD驱动入口函数,需要以下4步: 1) 分配一个fb_info结构体: framebuffer_alloc(); 2) 设置...

3079
来自专栏编程坑太多

RN请求豆瓣数据

1693
来自专栏移动开发之家

Flutter完整开发实战详解(二、 快速开发实战篇)

 作为系列文章的第二篇,继《Flutter完整开发实战详解(一、Dart语言和Flutter基础)》之后,本篇将为你着重展示:如何搭建一个通用的Flutter ...

1.4K1
来自专栏听雨堂

子线程调用UI线程的方法

vs2005中,子线程不允许使用UI中的控件,网上的解决方法都有:使用控件的Invoke,不过在我自己的应用中总觉得麻烦:我要从子线程中调用一个主线程中的处理...

2098
来自专栏互联网杂技

React -- 组件间通信

分为三种类型的通信关系: 1、父组件向子组件通信 2、子组件向父组件通信 3、没有嵌套关系的组件之间的通信 父组件向子组件通信 父组件通过子组件的prop...

4357
来自专栏拂晓风起

cocos2d-js 自定义事件监听派发

1393
来自专栏一枝花算不算浪漫

ajax图片上传及FastDFS入门案例.

47611
来自专栏技术墨客

React 渲染性能优化

在React内部已经使用了许多巧妙的技术来最小化由于Dom变更导致UI渲染所耗费的时间。对于很多应用来说,使用React后无需太多工作就会让客户端执行性能有质的...

913
来自专栏软件开发

JavaScript学习总结(五)——jQuery插件开发与发布

jQuery插件就是以jQuery库为基础衍生出来的库,jQuery插件的好处是封装功能,提高了代码的复用性,加快了开发速度,现在网络上开源的jQuery插件非...

963
来自专栏技术墨客

React学习(7)—— 高阶应用:性能优化 原

在React内部已经使用了许多巧妙的技术来最小化由于Dom变更导致UI渲染所耗费的时间。对于很多应用来说,使用React后无需太多工作就会让客户端执行性能有质的...

1312

扫码关注云+社区

领取腾讯云代金券