Skip to main content

futu_opend/startup/phase4/
network_exposure.rs

1use crate::config::RuntimeConfig;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub(super) struct LegacyNetworkExposureWarning {
5    pub(super) surface: &'static str,
6    pub(super) key_flag: &'static str,
7}
8
9fn is_loopback_bind(ip: &str) -> bool {
10    matches!(ip, "127.0.0.1" | "localhost" | "::1") || ip.starts_with("127.")
11}
12
13pub(super) fn legacy_network_exposure_warnings(
14    ip: &str,
15    any_keys_configured: bool,
16    allow_tcp_unauthenticated: bool,
17    rest_key_configured: Option<bool>,
18    grpc_key_configured: Option<bool>,
19    ws_key_configured: Option<bool>,
20) -> Vec<LegacyNetworkExposureWarning> {
21    if is_loopback_bind(ip) {
22        return Vec::new();
23    }
24
25    let mut warnings = Vec::new();
26    if !any_keys_configured || allow_tcp_unauthenticated {
27        warnings.push(LegacyNetworkExposureWarning {
28            surface: "FTAPI TCP",
29            key_flag: "--rest-keys-file/--grpc-keys-file/--ws-keys-file",
30        });
31    }
32    for (enabled_keyed, surface, key_flag) in [
33        (rest_key_configured, "REST", "--rest-keys-file"),
34        (grpc_key_configured, "gRPC", "--grpc-keys-file"),
35        (ws_key_configured, "WS", "--ws-keys-file"),
36    ] {
37        if enabled_keyed == Some(false) {
38            warnings.push(LegacyNetworkExposureWarning { surface, key_flag });
39        }
40    }
41    warnings
42}
43
44pub(super) fn telnet_exposure_warning(
45    ip: &str,
46    port: Option<u16>,
47) -> Option<LegacyNetworkExposureWarning> {
48    (port.is_some() && !is_loopback_bind(ip)).then_some(LegacyNetworkExposureWarning {
49        surface: "Telnet",
50        key_flag: "--telnet-ip",
51    })
52}
53
54pub(super) fn warn_legacy_network_exposure(config: &RuntimeConfig, any_keys_configured: bool) {
55    for warning in legacy_network_exposure_warnings(
56        &config.ip,
57        any_keys_configured,
58        config.allow_tcp_unauthenticated,
59        config.rest_port.map(|_| config.rest_keys_file.is_some()),
60        config.grpc_port.map(|_| config.grpc_keys_file.is_some()),
61        config.websocket_port.map(|_| config.ws_keys_file.is_some()),
62    ) {
63        tracing::warn!(
64            bind_ip = %config.ip,
65            surface = warning.surface,
66            key_flag = warning.key_flag,
67            "legacy network exposure: surface is reachable on a non-loopback bind without API-key auth"
68        );
69        eprintln!(
70            "⚠️  {} legacy mode is reachable on {} without API-key auth. \
71             For production, bind 127.0.0.1 or configure {}.",
72            warning.surface, config.ip, warning.key_flag
73        );
74    }
75
76    if let Some(warning) = telnet_exposure_warning(&config.telnet_ip, config.telnet_port) {
77        tracing::warn!(
78            bind_ip = %config.telnet_ip,
79            port = config.telnet_port,
80            surface = warning.surface,
81            key_flag = warning.key_flag,
82            "telnet management port is reachable on a non-loopback bind without API-key auth"
83        );
84        eprintln!(
85            "⚠️  Telnet management port is reachable on {} without API-key auth. \
86             For production, omit --telnet-port or bind it to 127.0.0.1 with --telnet-ip.",
87            config.telnet_ip
88        );
89    }
90}