/* SPDX-License-Identifier: MIT
*
* Phobos
*/
package phobos
import (
"encoding/binary"
"hash/crc32"
"net/netip"
)
const (
stunBindingRequest = 0x0001
stunBindingResponse = 0x0101
stunDataIndication = 0x0115
stunAttrXORMapped = 0x0020
stunAttrFingerprint = 0x8028
stunAttrData = 0x0013
stunHeaderSize = 20
stunDataIndHeaderSize = 24
stunBindingReqSize = stunHeaderSize + 8
stunFingerprintXOR = 0x5354554E
)
var stunCookie = [4]byte{0x21, 0x12, 0xA4, 0x42}
func stunHasMagic(buf []byte) bool {
return len(buf) >= 8 && [4]byte(buf[4:8]) == stunCookie
}
func stunMessageType(buf []byte) uint16 {
return binary.BigEndian.Uint16(buf)
}
func stunWriteHeader(buf []byte, messageType, messageLength uint16, txid []byte) {
binary.BigEndian.PutUint16(buf, messageType)
binary.BigEndian.PutUint16(buf[2:], messageLength)
copy(buf[4:8], stunCookie[:])
copy(buf[8:20], txid)
}
func stunWriteFingerprint(packet []byte, offset int) int {
binary.BigEndian.PutUint16(packet[offset:], stunAttrFingerprint)
binary.BigEndian.PutUint16(packet[offset+2:], 4)
binary.BigEndian.PutUint32(packet[offset+4:], crc32.ChecksumIEEE(packet[:offset])^stunFingerprintXOR)
return 8
}
func stunWriteXORMappedAddress(buf []byte, addr netip.AddrPort) int {
binary.BigEndian.PutUint16(buf, stunAttrXORMapped)
binary.BigEndian.PutUint16(buf[2:], 8)
buf[4] = 0
buf[5] = 0x01
binary.BigEndian.PutUint16(buf[6:], addr.Port())
buf[6] ^= stunCookie[0]
buf[7] ^= stunCookie[1]
ip := addr.Addr().As4()
for i := range 4 {
buf[8+i] = ip[i] ^ stunCookie[i]
}
return 12
}
func stunBuildBindingRequest(buf []byte, rng *rng32) int {
var txid [12]byte
rng.fill(txid[:])
stunWriteHeader(buf, stunBindingRequest, 0, txid[:])
length := stunWriteFingerprint(buf, stunHeaderSize)
binary.BigEndian.PutUint16(buf[2:], uint16(length))
return stunHeaderSize + length
}
func stunBuildBindingSuccess(buf, txid []byte, addr netip.AddrPort) int {
if !addr.Addr().Is4() && !addr.Addr().Is4In6() {
return -1
}
stunWriteHeader(buf, stunBindingResponse, 0, txid)
length := stunWriteXORMappedAddress(buf[stunHeaderSize:], addr)
length += stunWriteFingerprint(buf, stunHeaderSize+length)
binary.BigEndian.PutUint16(buf[2:], uint16(length))
return stunHeaderSize + length
}
func stunWrapDataIndication(buf []byte, length int, rng *rng32) int {
if length+stunDataIndHeaderSize > len(buf) {
return -1
}
copy(buf[stunDataIndHeaderSize:stunDataIndHeaderSize+length], buf[:length])
var txid [12]byte
rng.fill(txid[:])
stunWriteHeader(buf, stunDataIndication, 0, txid[:])
binary.BigEndian.PutUint16(buf[20:], stunAttrData)
binary.BigEndian.PutUint16(buf[22:], uint16(length))
return stunDataIndHeaderSize + length
}
func stunUnwrapDataIndication(buf []byte, length int) int {
if length < stunDataIndHeaderSize {
return -1
}
if stunMessageType(buf) != stunDataIndication {
return -1
}
if int(binary.BigEndian.Uint16(buf[2:]))+stunHeaderSize > length {
return -1
}
if binary.BigEndian.Uint16(buf[20:]) != stunAttrData {
return -1
}
dataLength := int(binary.BigEndian.Uint16(buf[22:]))
if dataLength+stunDataIndHeaderSize > length {
return -1
}
copy(buf[:dataLength], buf[stunDataIndHeaderSize:stunDataIndHeaderSize+dataLength])
return dataLength
}
func stunHandleIncoming(buf []byte, length int, src netip.AddrPort, sendBack SendFunc) int {
switch stunMessageType(buf) {
case stunBindingRequest:
if length < stunHeaderSize {
return -1
}
var txid [12]byte
copy(txid[:], buf[8:20])
responseLength := stunBuildBindingSuccess(buf, txid[:], src)
if responseLength > 0 {
sendBack(buf[:responseLength])
}
return 0
case stunBindingResponse:
return 0
case stunDataIndication:
return stunUnwrapDataIndication(buf, length)
default:
return 0
}
}