获取本地公网IP是一个常见的需求,通过HTTP(S)协议获取公网IP的服务并不少见。但大多是个人公益维护的,稳定性有待商榷。同时,知名的几个都部署在境外。众所周知,我国到境外的网络连接质量很差。
STUN(Session Traversal Utilities for NAT)协议被设计用于NAT穿透,获取客户端的公网IP与连接端口正是其功能之一。
许多大厂都会维护自己STUN服务器,稳定性与延迟都能得到保证,例如:
stun.qq.com:3478
stun.chat.bilibili.com:3478
stun.cloudflare.com:3478
stun.l.google.com:19302
本文将会介绍一部分STUN协议细节,并用Golang实现一个通过STUN协议获取公网IP的小工具。
STUN基于UDP传输,是典型的C/S模式。根据RFC定义,所有STUN报文信息都有一个固定头:
STUN Message Structure
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0 0| STUN Message Type | Message Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Magic Cookie |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Transaction ID (96 bits) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
懒癌犯了,先丢个代码。
func getPublicIP(IPver int) (string, error) {
if IPver != 4 && IPver != 6 {
return "", fmt.Errorf("invalid IP version %d, excepted 4 or 6\n", IPver)
}
stunServer := "stun.cloudflare.com:3478"
rand.Seed(time.Now().UnixNano())
conn, err := net.Dial(fmt.Sprintf("udp%d", IPver), stunServer)
if err != nil {
if strings.HasSuffix(err.Error(), "network is unreachable") {
return "", fmt.Errorf("no IPv%d", IPver)
}
if strings.HasSuffix(err.Error(), "no suitable address found") {
return "", fmt.Errorf("the STUN server doesn't support IPv%d", IPver)
}
return "", err
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(3 * time.Second))
// https://www.rfc-editor.org/rfc/rfc5389.html#section-6
// STUN Message Structure
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |0 0| STUN Message Type | Message Length |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Magic Cookie |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | |
// | Transaction ID (96 bits) |
// | |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// STUN message header
buf := new(bytes.Buffer)
// Start with fixed 0x00, message type: 0x01, message length: 0x0000
buf.Write([]byte{0x00, 0x01, 0x00, 0x00})
magicCookie := []byte{0x21, 0x12, 0xA4, 0x42}
buf.Write(magicCookie)
transactionID := make([]byte, 12)
rand.Read(transactionID)
buf.Write(transactionID)
_, err = conn.Write(buf.Bytes())
if err != nil {
return "", err
}
response := make([]byte, 1024)
n, err := conn.Read(response)
if err != nil {
return "", err
}
if n < 32 {
return "", fmt.Errorf("invalid response")
}
// Parse STUN message
if !bytes.Equal(response[4:8], buf.Bytes()[4:8]) {
return "", fmt.Errorf("invalid magic cookie in response")
}
if !bytes.Equal(response[8:20], buf.Bytes()[8:20]) {
return "", fmt.Errorf("transaction ID mismatch in response")
}
// https://www.rfc-editor.org/rfc/rfc5389.html#section-15
// STUN Attributes
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Type | Length |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Value (variable) ....
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// Parse STUN attributes
attributes := response[20:]
attrType := binary.BigEndian.Uint16(attributes[:2])
// Mapped Address && Xor-Mapped Address
if attrType != 0x0001 && attrType != 0x0020 {
return "", fmt.Errorf("invalid address attribute type")
}
attrLength := binary.BigEndian.Uint16(attributes[2:4])
if attrLength < 8 {
return "", fmt.Errorf("invalid address attribute length")
}
// https://www.rfc-editor.org/rfc/rfc5389.html#section-15.1
// MAPPED-ADDRESS
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |0 0 0 0 0 0 0 0| Family | Port |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | |
// | Address (32 bits or 128 bits) |
// | |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// https://www.rfc-editor.org/rfc/rfc5389.html#section-15.2
// XOR-MAPPED-ADDRESS
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |x x x x x x x x| Family | X-Port |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | X-Address (Variable)
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
attributeValue := attributes[4 : 4+attrLength]
family := attributeValue[1]
var ip []byte
switch family {
case 1:
ip = attributeValue[4:8]
case 2:
ip = attributeValue[4:20]
default:
return "", fmt.Errorf("unknown address family")
}
if attrType == 0x0020 { // XOR-Mapped Address
for i := 0; i < 4; i++ {
ip[i] ^= magicCookie[i]
}
if family == 2 {
for i := 4; i < len(ip); i++ {
ip[i] ^= transactionID[i-4]
}
}
}
return net.IP(ip).String(), nil
}