discover method

Future<StunResponse> discover()

Discovers external IP address and port

Implementation

Future<StunResponse> discover() async {
  final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
  // print('Bound socket to port ${socket.port}');

  final completer = Completer<StunResponse>();
  Timer? timeoutTimer;

  void cleanup() {
    timeoutTimer?.cancel();
    socket.close();
  }

  try {
    final request = StunMessage.createBindingRequest();
    final server = await stunServer;

    // Set up timeout for STUN response only
    timeoutTimer = Timer(timeout, () {
      if (!completer.isCompleted) {
        // print('STUN request timed out');
        cleanup();
        completer.completeError(
          TimeoutException('STUN request timed out after ${timeout.inSeconds} seconds'));
      }
    });

    // Listen for response
    socket.listen(
      (event) {
        if (event == RawSocketEvent.read) {
          final datagram = socket.receive();
          if (datagram == null) return;

          // print('Received response from ${datagram.address.address}:${datagram.port}');
          final response = StunMessage.decode(datagram.data);
          if (response == null) {
            // print('Failed to decode STUN response');
            return;
          }

          // Extract mapped address from XOR-MAPPED-ADDRESS or MAPPED-ADDRESS
          final mappedAddress = _extractMappedAddress(response);
          if (mappedAddress != null && !completer.isCompleted) {
            // print('Successfully extracted mapped address: ${mappedAddress.address.address}:${mappedAddress.port}');
            cleanup();
            completer.complete(StunResponse(
              externalAddress: mappedAddress.address,
              externalPort: mappedAddress.port,
              natType: _determineNatType(response),
            ));
          }
        }
      },
      onError: (error) {
        print('Socket error: $error');
        if (!completer.isCompleted) {
          cleanup();
          completer.completeError(error);
        }
      },
      onDone: () {
        print('Socket closed');
        if (!completer.isCompleted) {
          cleanup();
          completer.completeError(
            TimeoutException('Socket closed before receiving response'));
        }
      },
    );

    // Send request
    final requestData = request.encode();
    // print('Sending ${requestData.length} bytes to ${server.address}:$stunPort');
    final sent = socket.send(requestData, server, stunPort);
    // print('Sent $sent bytes');
    if (sent == 0) {
      throw Exception('Failed to send STUN request');
    }

    return await completer.future;
  } catch (e) {
    print('Error in discover: $e');
    cleanup();
    rethrow;
  }
}