作者: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
}