performStunRequest method

  1. @override
Future<StunResponse> performStunRequest()
override

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');
    },
  );
}