Golang实现STUN服务器时遇到的问题

Golang实现STUN服务器时遇到的问题 大家好,我最近在编写一些关于P2P通信的代码,需要进行NAT识别。根据RFC3893规范,当发送第一个绑定请求时,服务器应该使用不同的本地地址回复客户端,这意味着我需要更改UDP套接字的本地地址。我的问题是,该如何实现这一点?

以下是我在服务端测试更改本地地址的代码,但客户端无法收到回复。

package main

import (
	"net"
	"qiniupkg.com/x/log.v7"
	"strconv"
	"time"
)

func main(){
	buf := make([]byte,1024)
	laddr ,_ := net.ResolveUDPAddr("udp4","192.168.2.100:4261")
	lis,e := net.ListenUDP("udp4",laddr)
	if nil != e{
		log.Print(e.Error())
	}
	go func() {

		for true{
			lis.SetReadDeadline(time.Now().Add(time.Second*10))
			n, addr,e := lis.ReadFrom(buf)
			if nil != e{
				log.Print(e.Error())
			}
			log.Print("recv " + strconv.Itoa(n) + " from: " + addr.String() )
			laddr ,_ := net.ResolveUDPAddr("udp4","192.168.2.100:122")
			raddr ,_ :=	net.ResolveUDPAddr("udp4",addr.String())
			cnn, e := net.DialUDP("udp4",laddr,raddr)
			if nil !=e {
				log.Print(e.Error())
			}
			cnn.WriteTo([]byte("test"),raddr)
			//raddr ,_ := net.ResolveUDPAddr("udp4","127.0.0.1:134")

		}
	}()

	laddr ,_ = net.ResolveUDPAddr("udp4","192.168.2.100:1122")
	raddr ,_ := net.ResolveUDPAddr("udp4","192.168.2.100:4261")

	cnn, e := net.DialUDP("udp4",laddr,raddr)
	if nil !=e {
		log.Print(e.Error())
	}

	for true{
		buf := make([]byte,1024)
		cnn.Write([]byte{97,98,99})
		time.Sleep(time.Second)
		n,addr,_  := cnn.ReadFrom(buf)
		log.Print(addr)
		log.Print(buf[0:n])
	}
}

更多关于Golang实现STUN服务器时遇到的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang实现STUN服务器时遇到的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在STUN服务器实现中,根据RFC3893规范,服务器需要从不同的本地地址回复绑定请求以进行NAT检测。您的问题在于使用了DialUDP创建新连接,这会导致源端口改变,但STUN协议要求使用相同的UDP套接字进行通信。

以下是正确的实现方式:

package main

import (
    "encoding/binary"
    "net"
    "time"
)

// STUN消息类型常量
const (
    bindingRequest     = 0x0001
    bindingResponse    = 0x0101
    bindingErrorResp   = 0x0111
)

// STUN消息头结构
type stunHeader struct {
    msgType   uint16
    msgLength uint16
    magicCookie uint32
    transactionID [12]byte
}

func handleSTUNRequest(conn *net.UDPConn, clientAddr *net.UDPAddr, data []byte) {
    if len(data) < 20 {
        return
    }

    // 解析STUN消息头
    msgType := binary.BigEndian.Uint16(data[0:2])
    
    // 只处理绑定请求
    if msgType != bindingRequest {
        return
    }

    // 获取服务器所有本地地址
    localAddrs, err := net.InterfaceAddrs()
    if err != nil {
        return
    }

    // 为每个不同的本地地址发送响应
    for _, addr := range localAddrs {
        ipNet, ok := addr.(*net.IPNet)
        if !ok || ipNet.IP.IsLoopback() || ipNet.IP.To4() == nil {
            continue
        }

        // 创建STUN响应消息
        response := make([]byte, 20)
        binary.BigEndian.PutUint16(response[0:2], bindingResponse)
        binary.BigEndian.PutUint16(response[2:4], 0) // 长度先设为0
        binary.BigEndian.PutUint32(response[4:8], 0x2112A442) // STUN magic cookie
        
        // 复制事务ID
        copy(response[8:20], data[8:20])

        // 添加XOR-MAPPED-ADDRESS属性
        xorMappedAddr := createXORMappedAddress(clientAddr.IP, clientAddr.Port, 0x2112A442)
        response = append(response, xorMappedAddr...)

        // 更新消息长度
        binary.BigEndian.PutUint16(response[2:4], uint16(len(response)-20))

        // 使用相同的UDP连接发送响应
        _, err := conn.WriteToUDP(response, clientAddr)
        if err != nil {
            continue
        }
    }
}

func createXORMappedAddress(ip net.IP, port int, magicCookie uint32) []byte {
    attr := make([]byte, 8+len(ip))
    
    // 属性类型: XOR-MAPPED-ADDRESS
    binary.BigEndian.PutUint16(attr[0:2], 0x0020)
    
    // 属性长度
    binary.BigEndian.PutUint16(attr[2:4], uint16(4+len(ip)))
    
    // 地址族和端口
    if ip.To4() != nil {
        attr[4] = 0x01 // IPv4
        xorPort := uint16(port) ^ uint16(magicCookie>>16)
        binary.BigEndian.PutUint16(attr[6:8], xorPort)
        
        // XOR IP地址
        ipBytes := ip.To4()
        for i := 0; i < 4; i++ {
            attr[8+i] = ipBytes[i] ^ byte(magicCookie>>uint(24-i*8))
        }
    }
    
    return attr
}

func main() {
    serverAddr, err := net.ResolveUDPAddr("udp4", "192.168.2.100:3478")
    if err != nil {
        panic(err)
    }

    conn, err := net.ListenUDP("udp4", serverAddr)
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    buffer := make([]byte, 1024)
    
    for {
        conn.SetReadDeadline(time.Now().Add(10 * time.Second))
        n, clientAddr, err := conn.ReadFromUDP(buffer)
        if err != nil {
            if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
                continue
            }
            break
        }

        go handleSTUNRequest(conn, clientAddr, buffer[:n])
    }
}

关键点说明:

  1. 使用相同的UDP连接:STUN服务器应该使用conn.WriteToUDP()从同一个UDP套接字发送响应,而不是创建新的连接。

  2. 多地址响应:服务器需要从所有可用的本地IP地址发送响应,这通过遍历net.InterfaceAddrs()实现。

  3. STUN协议格式:正确构建STUN消息头,包含magic cookie和事务ID。

  4. XOR-MAPPED-ADDRESS:按照RFC5389规范,使用XOR操作处理映射地址。

  5. 并发处理:使用goroutine处理每个请求,避免阻塞主循环。

您的测试代码中创建新连接的方式破坏了STUN协议的要求。正确的做法是始终使用原始的监听连接进行通信,通过不同的源IP地址(而不是不同的端口)来实现RFC3893要求的地址变更测试。

回到顶部