futu_opend/startup/phase4/
network_exposure.rs1use 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}