nvpn 4.1.13

CLI and daemon for Nostr VPN private mesh networks
#[cfg(feature = "paid-exit")]
mod cashu_wallet_daemon;
mod config_bootstrap;
use nvpn::control_pubsub_runtime;
mod daemon_runtime;
mod diagnostics;
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod fips_host_tunnel;
mod fips_private_mesh;
#[cfg(unix)]
mod join_request_ipc;
#[cfg(target_os = "linux")]
mod linux_network;
#[cfg(any(target_os = "macos", test))]
mod macos_network;
#[cfg(any(target_os = "macos", test))]
mod macos_service;
mod network_signaling;
mod pairing_qr;
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
mod pipeline_profile;
mod platform_routing;
mod recent_peers_store;
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
mod secure_dns_runtime;
mod service_management;
mod session_runtime;
mod updater;
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
mod wg_upstream_runtime;
#[cfg(target_os = "windows")]
mod windows_network;
#[cfg(any(target_os = "windows", test))]
mod windows_tunnel;
#[cfg(target_os = "linux")]
mod wireguard_exit;

#[cfg(all(target_os = "linux", target_env = "musl"))]
#[global_allocator]
static GLOBAL_ALLOCATOR: mimalloc::MiMalloc = mimalloc::MiMalloc;

use fips_core::discovery::nostr::{OverlayEndpointAdvert, OverlayTransportKind};
use std::collections::{HashMap, HashSet};
#[cfg(target_os = "windows")]
use std::ffi::OsString;
use std::fs;
use std::fs::OpenOptions;
#[cfg(any(target_os = "macos", test))]
use std::hash::{Hash, Hasher};
#[cfg(feature = "paid-exit")]
use std::io::Read;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
#[cfg(feature = "paid-exit")]
use std::net::{ToSocketAddrs, UdpSocket};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command as ProcessCommand, Stdio};
#[cfg(test)]
use std::sync::Mutex;
#[cfg(any(target_os = "windows", test))]
use std::sync::OnceLock;
#[cfg(test)]
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use anyhow::{Context, Result, anyhow};
#[cfg(feature = "paid-exit")]
use cashu_service::{
    CashuSpilmanPayment, CashuSpilmanPaymentSigner, CashuSpilmanReceiverCloseResult,
    CashuWalletOverview, FileSpilmanPaymentReceiver, FileSpilmanPaymentReceiverConfig,
    FileSpilmanPaymentSigner, SharedSpilmanClientStoreLock, StreamingRouteCashuTokenLease,
    StreamingRouteOpenCashuSpilmanChannelFromWalletRequest, StreamingRoutePaymentEnvelope,
    StreamingRoutePaymentPayload, load_or_create_cashu_spilman_receiver_key, normalize_mint_url,
    spilman_client_store_path,
};
use clap::{Args, Parser, Subcommand, ValueEnum};
#[cfg(all(feature = "paid-exit", test))]
use nostr_sdk::prelude::{Alphabet, Filter, SingleLetterTag};
#[cfg(feature = "paid-exit")]
use nostr_sdk::prelude::{Event, EventBuilder, Keys, Kind, PublicKey, Tag, Timestamp, ToBech32};
#[cfg(feature = "paid-exit")]
use nostr_vpn_core::config::normalize_relay_urls;
#[cfg(feature = "paid-exit")]
use nostr_vpn_core::config::write_private_file_preserving_user_owner;
use nostr_vpn_core::config::{
    AppConfig, ExitDnsMode, ExitDohProvider, InternetSource, SharedNetworkRoster,
    derive_mesh_tunnel_ip, exit_node_default_routes, maybe_autoconfigure_node,
    normalize_advertised_route, normalize_fips_peer_endpoint_hint, normalize_nostr_pubkey,
    normalize_runtime_network_id, parse_wireguard_exit_config,
};
use nostr_vpn_core::control::PeerAnnouncement;
use nostr_vpn_core::data_plane::MeshPeerStatus;
use nostr_vpn_core::diagnostics::{
    HealthIssue, HealthSeverity, NetworkSummary, PortMappingStatus, ProbeState,
};
use nostr_vpn_core::fips_control::{
    JoinRosterControl, NetworkRoster, PeerCapabilities, PeerEndpointHint, SignedRoster,
    local_fips_dataplane_features,
};
#[cfg(feature = "paid-exit")]
use nostr_vpn_core::fips_mesh::FipsPaidRouteAdmission;
use nostr_vpn_core::join_requests::{FIPS_JOIN_REQUEST_RETRY_SECS, MeshJoinRequest};
use nostr_vpn_core::magic_dns::{
    MagicDnsResolverConfig, MagicDnsServer, build_magic_dns_records, install_system_resolver,
    uninstall_system_resolver,
};
#[cfg(feature = "paid-exit")]
use nostr_vpn_core::paid_route_probe::{
    DEFAULT_PAID_ROUTE_BANDWIDTH_BYTES, DEFAULT_PAID_ROUTE_DOWNLOAD_URL,
    DEFAULT_PAID_ROUTE_GEOIP_URL_TEMPLATE, DEFAULT_PAID_ROUTE_PUBLIC_IP_URL,
    DEFAULT_PAID_ROUTE_UPLOAD_URL, PaidRouteProbeMeasurement, PaidRouteProbeSample,
    build_paid_route_probe_measurement, paid_route_bandwidth_bps, paid_route_download_url,
    paid_route_geoip_url, paid_route_stun_binding_request, paid_route_stun_host_port,
    paid_route_stun_transaction_id, parse_paid_route_geoip_response,
    parse_paid_route_public_ip_response, parse_paid_route_stun_binding_response,
};
#[cfg(feature = "paid-exit")]
use nostr_vpn_core::paid_route_store::{
    ApplyPaidRouteSellerPaymentRequest, ApplyPaidRouteSellerSessionOpenRequest,
    AttachPaidRouteBuyerSpilmanChannelRequest, BuildPaidRouteBuyerPaymentEnvelopeKind,
    BuildPaidRouteBuyerPaymentEnvelopeRequest, BuildPaidRouteBuyerPaymentEnvelopeResult,
    BuildPaidRouteBuyerSignedPaymentEnvelopeRequest, BuildPaidRouteBuyerTokenLeaseEnvelopeRequest,
    OpenPaidRouteBuyerSessionRequest, OpenPaidRouteBuyerSessionResult,
    PaidRouteBuyerPaymentUpdateDue, PaidRouteBuyerPaymentUpdatesDueRequest, PaidRouteChannelRecord,
    PaidRouteChannelRole, PaidRouteLifecycleStatus, PaidRouteSellerCollectionState,
    PaidRouteSessionRecord, PaidRouteStore, RecordPaidRouteBuyerUsageRequest,
    RecordPaidRouteSellerUsageRequest, UpdatePaidRouteSessionProbeRequest,
    UpdatePaidRouteSessionProbeResult, load_paid_route_store, paid_route_store_file_path,
    update_paid_route_store, upsert_paid_route_offer,
};
#[cfg(feature = "paid-exit")]
use nostr_vpn_core::paid_routes::{
    ManualPaidExitProvider, PaidExitConfig, PaidExitUpstream, PaidRouteOffer, PaidRoutePaymentMode,
    PaidRouteQualityMetrics, PaidRouteRoutingDecision, PaidRouteSessionOpen, SignedPaidRouteOffer,
    paid_route_country_claim, paid_route_offer_filter,
    signed_paid_exit_offer_from_config_with_receiver,
    signed_paid_exit_offer_from_config_with_receiver_and_fips_endpoints,
};
#[cfg(target_os = "windows")]
use nostr_vpn_core::platform_paths::{
    legacy_config_path_from_dirs_config_dir, windows_default_config_path_for_state,
    windows_machine_config_path_from_program_data_dir,
    windows_service_config_path_from_sc_qc_output,
};
use nostr_vpn_core::signed_rosters::{
    load_signed_rosters, signed_rosters_file_path, upsert_signed_roster,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[cfg(target_os = "windows")]
use windows_service::define_windows_service;
#[cfg(target_os = "windows")]
use windows_service::service::{
    ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType,
};
#[cfg(target_os = "windows")]
use windows_service::service_control_handler::{self, ServiceControlHandlerResult};
#[cfg(target_os = "windows")]
use windows_service::service_dispatcher;

#[cfg(test)]
pub(crate) use crate::config_bootstrap::default_cli_install_path;
#[cfg(target_os = "windows")]
pub(crate) use crate::config_bootstrap::windows_service_install_config_path;
pub(crate) use crate::config_bootstrap::{
    apply_config_file, apply_devices_override, apply_participants_override, default_config_path,
    default_tunnel_iface, init_config, install_cli, load_config_read_only, load_or_default_config,
    print_version, uninstall_cli,
};
pub(crate) use crate::daemon_runtime::*;
use crate::diagnostics::{
    PortMappingRuntime, build_health_issues, capture_network_snapshot, detect_captive_portal,
    run_netcheck_report, write_doctor_bundle,
};
#[cfg(any(feature = "paid-exit", not(unix)))]
use crate::network_signaling::maybe_reload_running_daemon;
use crate::network_signaling::{
    RosterEditAction, reload_running_daemon_after_save, save_config_and_reload_transactionally,
    update_active_network_roster,
};
#[cfg(any(test, not(target_os = "windows")))]
pub(crate) use crate::platform_routing::*;
#[cfg(test)]
pub(crate) use crate::service_management::parse_nonzero_pid;
#[cfg(any(target_os = "windows", test))]
pub(crate) use crate::service_management::windows_should_apply_config_via_service;
#[cfg(test)]
pub(crate) use crate::service_management::{
    WindowsServiceState, WindowsServiceStopAction, windows_service_bin_path,
    windows_service_binary_path_from_sc_qc_output, windows_service_disabled_from_qc_output,
    windows_service_state_from_query_output, windows_service_status_from_query_output,
    windows_service_stop_action, windows_service_stop_failure_is_joinable,
};
#[cfg(test)]
pub(crate) use crate::service_management::{
    linux_service_binary_path, linux_service_config_path_from_unit_contents,
    linux_service_executable_path_from_unit_contents, linux_service_status_from_show_output,
    linux_service_unit_content, linux_service_unit_matches_config,
};
#[cfg(any(target_os = "macos", test))]
pub(crate) use crate::service_management::{xml_escape, xml_unescape};
pub(crate) use crate::session_runtime::*;
#[cfg(target_os = "linux")]
pub(crate) use crate::wireguard_exit::*;
const DAEMON_CONTROL_STOP_REQUEST: &str = "stop";
const DAEMON_CONTROL_RELOAD_REQUEST: &str = "reload";
const DAEMON_CONTROL_PAUSE_REQUEST: &str = "pause";
const DAEMON_CONTROL_RESUME_REQUEST: &str = "resume";
const DAEMON_STATUS_MODE_ENV: &str = "NVPN_DAEMON_STATUS_MODE";
const DAEMON_STATUS_MODE_STATE_FILE: &str = "state-file";
const DAEMON_STATE_RUNNING_MAX_AGE_SECS_ENV: &str = "NVPN_DAEMON_STATE_RUNNING_MAX_AGE_SECS";
const DEFAULT_DAEMON_STATE_RUNNING_MAX_AGE_SECS: u64 = 10;
const DAEMON_STATE_RUNNING_MAX_FUTURE_SKEW_SECS: u64 = 2;
const DAEMON_PEER_STATUS_MAX_FUTURE_SKEW_SECS: u64 = 2;
const MAJOR_LINK_CHANGE_TIME_JUMP_SECS: u64 = 30;
const WAITING_FOR_PARTICIPANTS_STATUS: &str = "Waiting for participants";
const LISTENING_FOR_JOIN_REQUESTS_STATUS: &str = "Listening for join requests";
const PRODUCT_VERSION: &str = env!("CARGO_PKG_VERSION");

pub(crate) struct DaemonJoinRequestIpcRequest {
    pub(crate) reset: bool,
    pub(crate) response: tokio::sync::oneshot::Sender<Result<String, String>>,
}

pub(crate) fn fips_core_build_version() -> String {
    fips_core::version::short_version().to_string()
}

#[cfg(target_os = "windows")]
const MAGIC_DNS_PORT: u16 = 53;
#[cfg(not(target_os = "windows"))]
const MAGIC_DNS_PORT: u16 = 1053;
#[cfg(any(target_os = "windows", test))]
const WINDOWS_DAEMON_STATE_FRESHNESS_SECS: u64 = 5;
#[cfg(any(target_os = "macos", test))]
const MACOS_SERVICE_LABEL: &str = "to.nostrvpn.nvpn";
#[cfg(target_os = "linux")]
const LINUX_SERVICE_UNIT_NAME: &str = "nvpn.service";
#[cfg(target_os = "windows")]
const WINDOWS_SERVICE_NAME: &str = "NvpnService";
#[cfg(target_os = "windows")]
const WINDOWS_SERVICE_DISPLAY_NAME: &str = "Nostr VPN";
#[cfg(target_os = "windows")]
const WINDOWS_SERVICE_DESCRIPTION: &str = "Nostr VPN background mesh and tunnel service";
#[cfg(target_os = "windows")]
const WINDOWS_SERVICE_STOP_TIMEOUT: Duration = Duration::from_secs(30);
#[cfg(target_os = "windows")]
static WINDOWS_SERVICE_DAEMON_ARGS: OnceLock<DaemonArgs> = OnceLock::new();
#[cfg(target_os = "windows")]
define_windows_service!(ffi_windows_service_main, windows_service_main);

include!("main/cli_args.rs");
include!("main/command_dispatch.rs");
include!("main/wg_self_test.rs");
include!("main/roster_sync.rs");
include!("main/status_types.rs");
#[cfg(feature = "paid-exit")]
include!("main/paid_exit.rs");
include!("main/runtime_helpers.rs");
include!("main/runtime_roster.rs");
include!("main/daemon_commands.rs");
include!("main/doctor_and_parsing.rs");

#[cfg(test)]
mod tests {
    #[path = "../tests/cli_smoke.rs"]
    mod cli_smoke;
    #[path = "../tests/config_cache.rs"]
    mod config_cache;
    #[path = "../tests/daemon_control.rs"]
    mod daemon_control;
    #[path = "../tests/runtime_misc.rs"]
    mod runtime_misc;
    #[cfg(feature = "paid-exit")]
    #[path = "../tests/runtime_misc_paid_exit_relay.rs"]
    mod runtime_misc_paid_exit_relay;
    #[path = "../tests/runtime_network_transitions.rs"]
    mod runtime_network_transitions;
    #[path = "../tests/service_cli.rs"]
    mod service_cli;
}