performStunRequest method
Performs a STUN binding request to discover the public IP and port
Implementation
@override
Future<StunResponse> performStunRequest() async {
// Create STUN binding request message
final request = StunMessage.createBindingRequest();
final requestBytes = request.toBytes();
// Determine IP version from socket
final isIPv6Socket = _socket.address.type == InternetAddressType.IPv6;
// Log local socket info
print('[StunHandler] Local socket: ${_socket.address}:${_socket.port}');
// Send request to STUN server
final stunServerAddr = await InternetAddress.lookup(
_stunAddress,
type: isIPv6Socket ? InternetAddressType.IPv6 : InternetAddressType.IPv4,
);
if (stunServerAddr.isEmpty) {
throw StateError('Could not resolve STUN server address: $_stunAddress');
}
final targetAddr = stunServerAddr.first;
print('[StunHandler] Sending STUN request to $targetAddr:$_stunPort');
_socket.send(requestBytes, targetAddr, _stunPort);
final completer = Completer<StunResponse>();
// Listen for response from STUN server
StreamSubscription<RawSocketEvent>? subscription;
subscription = _socket.listen((event) {
if (event == RawSocketEvent.read) {
final datagram = _socket.receive();
if (datagram == null) return;
// Parse STUN response
final stunResponse = StunMessage.fromBytes(datagram.data);
final xorMappedAddr = stunResponse.getXorMappedAddress();
// Extract public IP and port from XOR-MAPPED-ADDRESS
if (xorMappedAddr != null && !completer.isCompleted) {
final ipVersion = isIPv6Socket ? IpVersion.v6 : IpVersion.v4;
final response = (
publicIp: xorMappedAddr.ip,
publicPort: xorMappedAddr.port,
ipVersion: ipVersion,
transactionId: stunResponse.transactionId,
raw: datagram.data,
attrs: {'transactionId': stunResponse.transactionId},
);
// Format address correctly based on IP version
final addressDisplay = ipVersion == IpVersion.v6
? '[${response.publicIp}]:${response.publicPort}'
: '${response.publicIp}:${response.publicPort}';
print('[StunHandler] STUN response: $addressDisplay (${response.ipVersion.value})');
print('[StunHandler] Port mapping: Local ${_socket.port} -> Public ${response.publicPort}');
subscription?.cancel();
completer.complete(response);
}
}
});
// Timeout after 5 seconds if no response
return completer.future.timeout(
const Duration(seconds: 5),
onTimeout: () {
subscription?.cancel();
throw TimeoutException('[StunHandler] STUN request timed out');
},
);
}