2019年8月4日 星期日

Golang 實作 UDP 防火牆穿透

在遊戲即時通訊下,兩台不同網路下的電腦要互相連接,是很常見的連線問題,透過一個 UDP 防火牆穿透技術,可以從 NAT 其中一種網路架構 Port Restriction Cone ,實作直接連接通信。

使用防火牆穿透技術,這表示同常兩端的電腦都不是直接對外公開 IP ,而是從任何有規模的網路底下建立連接的必要步驟。


 UDP 穿透技術已經行之有年,整個實作的原理其實很簡單,但面對 NAT Translation 問題時,卻有很大的困難, 如上面所說的,UDP 防火牆穿透 (UDP Hole-punching) 只能在 Port Restriction Cone 的情形,大部分家用 Router 直接對連 PPPoE 的話,很有可能就是此種架構,稍後在測試時一定要注意是否自己是這種架構。


依照時序來看,真實的環境是:

  1.  HOME A 的 PC 1 ,連接 Public IP Server : OOO.OOO.OOO.OOO:5000 等待回傳另一端
  2.  HOME B 的 PC 1 ,連接 Public IP Server : OOO.OOO.OOO.OOO:5000 等待回傳另一端
OOO.OOO.OOO.OOO:5000 這台 Server 其實在第二台電腦連線時,就會把雙方資訊做交換了。

因此,在這個情境,要把 UDP Server 程式碼放在 OOO.OOO.OOO.OOO 這台在公開網路的電腦上。


udpserver.go:


package main

import (
	"fmt"
	"net"
)

func main() {
	listener, err := net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4zero,
		Port: 5000, //對外公開 Prt 5000
	})
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("Local: <%s> \n", listener.LocalAddr().String())

    //定義有兩台電腦要互相連接
	var a_computer, b_computer net.UDPAddr
    
    //等待第一台電腦連接
	a_computer = *waitAndRead(listener)
	//等待第二台電腦連接
    b_computer = *waitAndRead(listener)
	
    //把 b 電腦資訊傳給 a 電腦
	listener.WriteToUDP([]byte(b_computer.String()), &a_computer)
    //把 a 電腦資訊傳給 b 電腦
	listener.WriteToUDP([]byte(a_computer.String()), &b_computer)
	
    //伺服器可以掰掰了
	fmt.Println("Server Mission Clear.")
}

func waitAndRead(listener *net.UDPConn) *net.UDPAddr {

	buff := make([]byte, 1024)
	n, remoteAddr, err := listener.ReadFromUDP(buff)
	if err != nil {
		fmt.Printf("error during read %s", err)
	}

	fmt.Printf("<%s> %s\n", remoteAddr, buff[:n])

	//port := strconv.Itoa(remoteAddr.Port)
	return remoteAddr

}

Server 要做的事就是這麼簡單。

然後是兩台在不同區域的電腦下要互相連接,所要用的程式碼。

udpclient.go:

package main

import (
	"fmt"
	"net"
	"strconv"
	"strings"
	"time"
)

func main() {
    
    //選擇一個本地打洞的 port,我在這裡是固定為 5001 (注意,那表示下面連接時是固定連到對方的 5001,你不能在自己電腦開兩個 5001 的 app)
	srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 5001} //fixed same port for all computer, do not use 0 by dynamic
	
    //輸入公開網路那台 server 的 ip 以及那個 port (請替換 0.0.0.0)
    dstAddr := &net.UDPAddr{IP: net.ParseIP("0.0.0.0"), Port: 5000} 
	
    //先跟 server 連接
	conn, err := net.DialUDP("udp", srcAddr, dstAddr)
	if err != nil {
		fmt.Println(err)
	}
	
    //隨便傳一個訊息給 server ,目的是讓 server 知道我的 IP 地址
	//send msg to central public server(let server know my address)
	conn.Write([]byte("hello"))
	fmt.Println("Hole Punching, UDP Client to Public Server.")
	
    
    //強制等待 server 告訴我對方的 IP
	//receive another computer information...
	fmt.Println("Waiting for Public Server Return Message...")
	var another_computer *net.UDPAddr = waitAndReadAndParseUDP(conn)

	fmt.Println("Successfuly get another computer address!")
	fmt.Println("ADDRESS: ", another_computer.String())
	
    //連接成功,跟 server 斷線,準備跟另一台電腦連接
	fmt.Println("Close Original UDP Connection(Port release)")
	conn.Close()
	
    //直接連接對方電腦,使用的 port 是 5001 跟自己一樣
	//send message to other computer directly
	fmt.Println("Build a connection to another computer port and OPEN NAT PORT wait for receive message")
	another_conn, err := net.DialUDP("udp", srcAddr, another_computer)
	if err != nil {
		fmt.Println("Can't Build Hole Punching...")
		fmt.Println(err)
	}
	defer another_conn.Close()
	_, err = conn.Write([]byte("Hole Punching"))
	if err != nil {
		fmt.Println("Failed to send punched message")
		fmt.Println(err)
	}
	
    //建立連接之後,瘋狂送訊息
	fmt.Println("Keep Send Message")
	//keep send message...
	go func() {
		for {
			_, err = another_conn.Write([]byte("Hello This is Another One..."))
			if err != nil {
				fmt.Println("Failed to send message")
				fmt.Println(err)
			}
			time.Sleep(1 * time.Second)
			fmt.Println("Sent...")
		}
	}()
	
    //瘋狂接收對方訊息
	//keep receiving message
	for {
		buff := make([]byte, 1024)
		n, remoteAddr, err := another_conn.ReadFromUDP(buff)
		if err != nil {
			fmt.Printf("error during read %s", err)
		}

		fmt.Printf("<%s> %s\n", remoteAddr, buff[:n])
	}
}

func waitAndReadAndParseUDP(listener *net.UDPConn) *net.UDPAddr {

	buff := make([]byte, 1024)
	n, _, err := listener.ReadFromUDP(buff)
	if err != nil {
		fmt.Printf("error during read %s", err)
	}

	another_computer_address := strings.Split(string(buff[:n]), ":")
	port, _ := strconv.Atoi(another_computer_address[1])

	return &net.UDPAddr{
		IP:   net.ParseIP(another_computer_address[0]),
		Port: port,
	}

}


在這段程式中的缺點是 port 是固定的,如果要浮動各自電腦的 port,其實只要在跟 server 建立連接時,也把自己開的 port 傳出去給 server 幫忙交換資訊,也可以達到效果,只是因為我的電腦網路環境無法任意開 port,防火牆有擋住 Inbound rule,所以只能用 5001。


詳細其他穿透手段,可以參考 Reference。


Reference:

https://blog.csdn.net/chen3888015/article/details/7474922
https://www.zhihu.com/question/20436734
https://blog.csdn.net/jq0123/article/details/840302
https://portforward.com/nat-types/




沒有留言:

張貼留言

© Mac Taylor, 歡迎自由轉貼。
Background Email Pattern by Toby Elliott
Since 2014