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 warn_legacy_network_exposure(config: &RuntimeConfig, any_keys_configured: bool) {
45    for warning in legacy_network_exposure_warnings(
46        &config.ip,
47        any_keys_configured,
48        config.allow_tcp_unauthenticated,
49        config.rest_port.map(|_| config.rest_keys_file.is_some()),
50        config.grpc_port.map(|_| config.grpc_keys_file.is_some()),
51        config.websocket_port.map(|_| config.ws_keys_file.is_some()),
52    ) {
53        tracing::warn!(
54            bind_ip = %config.ip,
55            surface = warning.surface,
56            key_flag = warning.key_flag,
57            "legacy network exposure: surface is reachable on a non-loopback bind without API-key auth"
58        );
59        eprintln!(
60            "⚠️  {} legacy mode is reachable on {} without API-key auth. \
61             For production, bind 127.0.0.1 or configure {}.",
62            warning.surface, config.ip, warning.key_flag
63        );
64    }
65
66    if config.telnet_port.is_some() && !is_loopback_bind(&config.telnet_ip) {
67        tracing::warn!(
68            bind_ip = %config.telnet_ip,
69            port = config.telnet_port,
70            "telnet management port is reachable on a non-loopback bind without API-key auth"
71        );
72        eprintln!(
73            "⚠️  Telnet management port is reachable on {} without API-key auth. \
74             For production, omit --telnet-port or bind it to 127.0.0.1 with --telnet-ip.",
75            config.telnet_ip
76        );
77    }
78}