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
更多关于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])
}
}
关键点说明:
-
使用相同的UDP连接:STUN服务器应该使用
conn.WriteToUDP()从同一个UDP套接字发送响应,而不是创建新的连接。 -
多地址响应:服务器需要从所有可用的本地IP地址发送响应,这通过遍历
net.InterfaceAddrs()实现。 -
STUN协议格式:正确构建STUN消息头,包含magic cookie和事务ID。
-
XOR-MAPPED-ADDRESS:按照RFC5389规范,使用XOR操作处理映射地址。
-
并发处理:使用goroutine处理每个请求,避免阻塞主循环。
您的测试代码中创建新连接的方式破坏了STUN协议的要求。正确的做法是始终使用原始的监听连接进行通信,通过不同的源IP地址(而不是不同的端口)来实现RFC3893要求的地址变更测试。

