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 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}