clockwork-city/addons/tube/tube_network_diagnosis_peer.gd

169 lines
4.3 KiB
GDScript3
Raw Normal View History

2026-04-13 11:34:00 -05:00
class_name TubeNetworkDiagnosisPeer extends RefCounted
signal warning_raised(message: String)
signal nat_hole_punching_compliance_updated(compliance: Compliance)
const WAITING_TIME := 10.0 #sec
const STUN_BINDING_REQUEST := 0x0001
const STUN_MAGIC_COOKIE := 0x2112A442
# STUN attribute types
const ATTR_XOR_MAPPED_ADDRESS := 0x0020
const ATTR_MAPPED_ADDRESS := 0x0001
#const ATTR_USERNAME := 0x0006
#const ATTR_MESSAGE_INTEGRITY := 0x0008
#const ATTR_ERROR_CODE := 0x0009
#const ATTR_UNKNOWN_ATTRIBUTES := 0x000A
#const ATTR_PRIORITY := 0x0024
#const ATTR_USE_CANDIDATE := 0x0025
#const ATTR_ICE_CONTROLLED := 0x8029
#const ATTR_ICE_CONTROLLING := 0x802A
enum Compliance {UNKNOWN, YES, NO}
var peer := PacketPeerUDP.new()
var _binding_port: int
var _binding_time: float = 0.0
var public_ports: Dictionary[String, int] = {} # destionnation address:port, public port
func _u16_to_bytes(v) -> PackedByteArray:
return PackedByteArray([v>>8 & 0xFF, v & 0xFF])
func _u32_to_bytes(v) -> PackedByteArray:
return PackedByteArray([v>>24 & 0xFF, v>>16 & 0xFF, v>>8 & 0xFF, v & 0xFF])
func _bytes_to_u16(bytes: PackedByteArray) -> int:
return (bytes[0] << 8) | bytes[1]
func _init(p_port: int) -> void:
_binding_port = p_port
func raise_warning(message: String):
push_warning(message)
warning_raised.emit(message)
# HOLE PUNCHING
func start_nat_hole_punching_detection(p_urls: Array[String]):
public_ports.clear()
if not peer.is_bound():
var error := peer.bind(_binding_port)
if error:
raise_warning("cannot bind to {port}: {error}".format({
"port": _binding_port,
"error": error_string(error)
}))
return
_binding_time = 0.0
for url in p_urls:
var splited := url.split(":")
var address := splited[1]
var port := int(splited[2])
_send_stunbinding_request(address, port)
func is_nat_hole_punching_compliant() -> Compliance:
var ports := {}
for i_port in public_ports.values():
ports[i_port] = 0
if 1 < len(ports): # multiple ports --> mapping depends on destination
return Compliance.NO
if 1 == len(ports) and 1 < len(public_ports): # one port and multiple destinations --> NAT preserves mapping across destinations
return Compliance.YES
return Compliance.UNKNOWN
func _send_stunbinding_request(p_address: String, p_port: int) -> void:
var error := peer.set_dest_address(p_address, p_port)
if error:
raise_warning("cannot set destionation address {address}:{port}: {error}".format({
"address": p_address,
"port": p_port,
"error": error_string(error)
}))
return
# Build STUN Binding Request packet (20 bytes header)
var packet = PackedByteArray()
packet.resize(0)
packet.append_array(_u16_to_bytes(STUN_BINDING_REQUEST)) # Type
packet.append_array(_u16_to_bytes(0)) # Message length 0 (no attributes)
packet.append_array(_u32_to_bytes(STUN_MAGIC_COOKIE)) # Magic Cookie
for i in 12: # Transaction ID (random)
packet.append(randi() & 0xFF)
error = peer.put_packet(packet)
if error:
raise_warning("cannot put packet to {address}:{port}: {error}".format({
"address": p_address,
"port": p_port,
"error": error_string(error)
}))
return
func parse_stun_response(data: PackedByteArray):
if data.size() < 20:
raise_warning("STUN response too short")
return
var i := 20 #Skip header
while i + 4 <= data.size():
var attr_type := (data[i] << 8) | data[i+1]
var attr_len := (data[i+2] << 8) | data[i+3]
var value := data.slice(i+4, i+4+attr_len)
i += 4 + attr_len
# Attributes are 32-bit aligned: pad to 4 bytes
if attr_len % 4 != 0:
i += 4 - (attr_len % 4)
# Decode port in attributes
var port: int
if attr_type == ATTR_XOR_MAPPED_ADDRESS and attr_len >= 8:
port = ((value[2] << 8) | value[3]) ^ (STUN_MAGIC_COOKIE >> 16)
elif attr_type == ATTR_MAPPED_ADDRESS and attr_len >= 8:
port = (value[2] << 8) | value[3]
else:
continue
var key := "{address}:{port}".format({
"address": peer.get_packet_ip(),
"port": peer.get_packet_port(),
})
public_ports[key] = port
return
# PROCESS
func _process(delta: float) -> void:
_binding_time += delta
while 0 < peer.get_available_packet_count():
var response = peer.get_packet()
parse_stun_response(response)
nat_hole_punching_compliance_updated.emit(is_nat_hole_punching_compliant())
if WAITING_TIME < _binding_time:
peer.close()