nvpn 4.1.13

CLI and daemon for Nostr VPN private mesh networks
pub(crate) async fn connect_vpn(args: ConnectArgs) -> Result<()> {
    if args.iface.trim().is_empty() {
        return Err(anyhow!("--iface must not be empty"));
    }

    let config_path = args.config.unwrap_or_else(default_config_path);
    let _instance_lock = acquire_daemon_instance_lock(&config_path)?;
    #[cfg(any(target_os = "macos", test))]
    crate::ensure_macos_connect_privileges(&config_path)?;
    if repair_saved_network_state(&config_path)
        .context("connect refused while saved network cleanup remains incomplete")?
    {
        transition_daemon_state_after_network_repair(&config_path)?;
    }
    let (mut app, mut network_id) = load_config_with_overrides(
        &config_path,
        args.network_id,
        args.devices,
        ConfigLoadMode::Persist,
    )?;
    let configured_participants = app.participant_pubkeys_hex();
    if configured_participants.is_empty() {
        return Err(anyhow!(
            "at least one participant must be configured before running connect"
        ));
    }
    let own_pubkey = app.own_nostr_pubkey_hex().ok();
    let mut expected_peers = expected_peer_count(&app);
    let iface = args.iface.clone();
    let network_snapshot = capture_network_snapshot();
    let mut port_mapping_runtime = PortMappingRuntime::default();
    refresh_port_mapping(
        &app,
        &network_snapshot,
        app.node.listen_port,
        &mut port_mapping_runtime,
    )
    .await;
    let (mut fips_tunnel_runtime, mut last_fips_endpoint_peer_signature) = {
        let config = fips_tunnel_config_from_app(
            FipsTunnelConfigInput {
                app: &app,
                config_path: &config_path,
                network_id: &network_id,
                iface: iface.clone(),
                underlay_interface: network_snapshot.default_interface.as_deref(),
                underlay_interface_mtu: network_snapshot.default_interface_mtu,
                own_pubkey: own_pubkey.as_deref(),
                recent_peers: None,
                live_peer_endpoints: &[],
                ethernet_underlay: None,
            },
        )?;
        let endpoint_peer_signature = endpoint_peer_signature(&config.endpoint_peers);
        let runtime = start_fips_private_tunnel_runtime(&config_path, config).await?;
        println!("connect: FIPS private mesh on {}", runtime.iface());
        (Some(runtime), endpoint_peer_signature)
    };
    let mut magic_dns_runtime = start_split_magic_dns(&app);

    println!(
        "connect: network {network_id} using FIPS private mesh; waiting for {expected_peers} configured peer(s)"
    );

    let mut announce_interval =
        tokio::time::interval(Duration::from_secs(args.mesh_refresh_interval_secs.max(5)));
    announce_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
    let mut tunnel_heartbeat_interval = tokio::time::interval(Duration::from_secs(2));
    tunnel_heartbeat_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
    let mut pending_fips_roster_recipients: HashSet<String> = HashSet::new();
    let mut fips_roster_sync_state = FipsRosterSyncState::default();
    let mut connect_status = String::new();

    let mut last_mesh_count = 0_usize;
    loop {
        tokio::select! {
            _ = tokio::signal::ctrl_c() => {
                break;
            }
            _ = tunnel_heartbeat_interval.tick() => {
                if let Some(runtime) = fips_tunnel_runtime.as_mut() {
                    let now = unix_timestamp();
                    let pending_manual_join = app
                        .active_network_opt()
                        .is_some_and(|network| network.local_identity_confirmation_pending);
                    let ping_result = if pending_manual_join {
                        runtime.ping_pending_join_peers(&network_id, now).await
                    } else {
                        runtime.ping_peers(&network_id, now).await
                    };
                    if let Err(error) = ping_result {
                        eprintln!("fips: peer ping failed: {error}");
                    }
                    if let Err(error) = runtime.refresh_link_statuses().await {
                        eprintln!("fips: peer link snapshot failed: {error}");
                    }
                    if let Err(error) = sync_fips_roster_with_connected_peers(
                        runtime,
                        &app,
                        &config_path,
                        &mut fips_roster_sync_state,
                    )
                    .await
                    {
                        eprintln!("fips: roster peer sync failed: {error}");
                    }
                    flush_pending_fips_roster_recipients(
                        runtime,
                        &app,
                        &config_path,
                        &mut pending_fips_roster_recipients,
                    );
                    match drain_fips_mesh_events(
                        runtime,
                        &mut app,
                        &config_path,
                        &mut connect_status,
                    )
                    .await
                    {
                        Ok(drained) => {
                            let roster_changed = drained.roster_changed;
                            network_id = app.effective_network_id();
                            expected_peers = expected_peer_count(&app);
                            if roster_changed {
                                if let Err(error) = refresh_fips_tunnel_config(
                                    runtime,
                                    &app,
                                    &config_path,
                                    &network_id,
                                    network_snapshot.default_interface.as_deref(),
                                    network_snapshot.default_interface_mtu,
                                    own_pubkey.as_deref(),
                                )
                                .await
                                {
                                    eprintln!("connect: roster applied, but FIPS reload failed: {error}");
                                } else if let Err(error) =
                                    broadcast_local_fips_capabilities(runtime, &app).await
                                {
                                    eprintln!(
                                        "fips: capabilities broadcast failed after roster apply: {error}"
                                    );
                                }
                                refresh_or_start_split_magic_dns(&mut magic_dns_runtime, &app);
                            }
                            if !drained.endpoint_hint_participants.is_empty()
                                && let Err(error) =
                                    refresh_fips_tunnel_runtime_peer_paths_in_place(
                                        runtime,
                                        FipsRestartContext {
                                            app: &app,
                                            config_path: &config_path,
                                            network_id: &network_id,
                                            fallback_iface: &iface,
                                            underlay_interface: network_snapshot
                                                .default_interface
                                                .as_deref(),
                                            underlay_interface_mtu: network_snapshot
                                                .default_interface_mtu,
                                            own_pubkey: own_pubkey.as_deref(),
                                            recent_peers: None,
                                            ethernet_underlay: None,
                                            client_dataplane_enabled: true,
                                            last_endpoint_peer_signature:
                                                &mut last_fips_endpoint_peer_signature,
                                        },
                                        &drained.endpoint_hint_participants,
                                        "fresh endpoint capability",
                                    )
                                    .await
                            {
                                eprintln!(
                                    "connect: FIPS endpoint hint refresh failed: {error}"
                                );
                            }
                        }
                        Err(error) => eprintln!("connect: FIPS event handling failed: {error}"),
                    }
                    if let Err(error) = runtime.refresh_peer_dependent_routes().await {
                        eprintln!("fips: peer route refresh failed: {error}");
                    }
                    maybe_log_fips_mesh_count(
                        &app,
                        own_pubkey.as_deref(),
                        &runtime.peer_statuses(),
                        expected_peers,
                        &mut last_mesh_count,
                    );
                }
            }
            _ = announce_interval.tick() => {
                if let Some(runtime) = fips_tunnel_runtime.as_ref() {
                    if let Err(error) = publish_fips_active_network_roster(
                        runtime,
                        &app,
                        &config_path,
                        &mut pending_fips_roster_recipients,
                    ) {
                        eprintln!("fips: roster publish failed: {error}");
                    }
                    if let Err(error) = broadcast_local_fips_capabilities(runtime, &app).await {
                        eprintln!("fips: capabilities broadcast failed: {error}");
                    }
                }
            }
        }
    }

    port_mapping_runtime.stop().await;
    if let Some(runtime) = fips_tunnel_runtime {
        stop_fips_private_tunnel_runtime(&config_path, runtime).await?;
    }
    println!("connect: disconnected");

    Ok(())
}