diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96b1ac70..669061c0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,6 +56,13 @@ jobs: - uses: taiki-e/install-action@cargo-hack + - name: Check Cargo.lock is up to date + run: | + if ! cargo metadata --format-version 1 --locked --no-deps > /dev/null; then + echo "::error::Cargo.lock is out of date. Run cargo generate-lockfile or cargo build locally, then commit Cargo.lock." + exit 1 + fi + - name: Check formatting run: cargo fmt --all -- --check @@ -144,4 +151,4 @@ jobs: steps: - name: Mark result as failed if: needs.test_matrix.result != 'success' - run: exit 1 \ No newline at end of file + run: exit 1 diff --git a/Cargo.lock b/Cargo.lock index 74142212..20277ced 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2191,6 +2191,7 @@ dependencies = [ "easytier-rpc-build", "encoding", "flume 0.12.0", + "forwarded-header-value", "futures", "futures-util", "gethostname 0.5.0", @@ -2208,6 +2209,7 @@ dependencies = [ "humantime-serde", "idna 1.0.3", "indoc", + "itertools 0.14.0", "kcp-sys", "machine-uid", "maplit", @@ -2920,6 +2922,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror 1.0.63", +] + [[package]] name = "fragile" version = "2.0.1" @@ -3770,7 +3782,7 @@ dependencies = [ "rustls-pki-types", "unicase", "webpki", - "webpki-roots", + "webpki-roots 0.26.3", "zeroize", ] @@ -3847,7 +3859,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 0.26.3", ] [[package]] @@ -4319,6 +4331,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -5205,6 +5226,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + [[package]] name = "normpath" version = "1.3.0" @@ -7137,7 +7164,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", + "webpki-roots 0.26.3", "windows-registry", ] @@ -8460,7 +8487,7 @@ dependencies = [ "tracing", "url", "uuid", - "webpki-roots", + "webpki-roots 0.26.3", ] [[package]] @@ -9558,9 +9585,9 @@ dependencies = [ [[package]] name = "tokio-websockets" -version = "0.8.3" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842e11addde61da7c37ef205cd625ebcd7b607076ea62e4698f06bfd5fd01a03" +checksum = "dad543404f98bfc969aeb71994105c592acfc6c43323fddcd016bb208d1c65cb" dependencies = [ "base64 0.22.1", "bytes", @@ -9571,10 +9598,11 @@ dependencies = [ "httparse", "ring", "rustls-pki-types", + "simdutf8", "tokio", "tokio-rustls", "tokio-util", - "webpki-roots", + "webpki-roots 1.0.6", ] [[package]] @@ -10675,6 +10703,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.0" diff --git a/easytier-web/frontend-lib/src/components/Config.vue b/easytier-web/frontend-lib/src/components/Config.vue index 1e3f4fcb..24c9e9bf 100644 --- a/easytier-web/frontend-lib/src/components/Config.vue +++ b/easytier-web/frontend-lib/src/components/Config.vue @@ -300,8 +300,21 @@ watch(() => curNetwork.value, syncNormalizedNetwork, { immediate: true, deep: fa - + + + + +
+
+
+ + +
+
diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml index 9dae3245..ce7102a5 100644 --- a/easytier-web/frontend-lib/src/locales/cn.yaml +++ b/easytier-web/frontend-lib/src/locales/cn.yaml @@ -196,6 +196,12 @@ mtu_help: | TUN设备的MTU,默认为非加密时为1380,加密时为1360。范围:400-1380 mtu_placeholder: 留空为默认值1380 +instance_recv_bps_limit: 实例接收限速 +instance_recv_bps_limit_help: | + 限制当前实例整体入站流量的总接收速率,单位为字节每秒。 + 留空表示不限速。 +instance_recv_bps_limit_placeholder: 留空表示不限速 + mapped_listeners: 监听映射 mapped_listeners_help: | 手动指定监听器的公网地址,其他节点可以使用该地址连接到本节点。 diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml index 5854059e..7f8d7b21 100644 --- a/easytier-web/frontend-lib/src/locales/en.yaml +++ b/easytier-web/frontend-lib/src/locales/en.yaml @@ -196,6 +196,12 @@ mtu_help: | MTU of the TUN device, default is 1380 for non-encryption, 1360 for encryption. Range:400-1380 mtu_placeholder: Leave blank as default value 1380 +instance_recv_bps_limit: Instance Receive Limit +instance_recv_bps_limit_help: | + Limit the total receive bandwidth for the whole instance. Unit: bytes per second. + Leave blank for no limit. +instance_recv_bps_limit_placeholder: Leave blank for no limit + mapped_listeners: Map Listeners mapped_listeners_help: | Manually specify the public address of the listener, other nodes can use this address to connect to this node. diff --git a/easytier-web/frontend-lib/src/types/network.ts b/easytier-web/frontend-lib/src/types/network.ts index b62b0e6b..4b3517eb 100644 --- a/easytier-web/frontend-lib/src/types/network.ts +++ b/easytier-web/frontend-lib/src/types/network.ts @@ -78,6 +78,7 @@ export interface NetworkConfig { socks5_port: number mtu: number | null + instance_recv_bps_limit: number | null mapped_listeners: string[] enable_magic_dns?: boolean @@ -146,6 +147,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { enable_socks5: false, socks5_port: 1080, mtu: null, + instance_recv_bps_limit: null, mapped_listeners: [], enable_magic_dns: false, enable_private_mode: false, diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index 586e8afa..c6be067a 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -217,6 +217,9 @@ core_clap: foreign_relay_bps_limit: en: "the maximum bps limit for foreign network relay, default is no limit. unit: BPS (bytes per second)" zh-CN: "作为共享节点时,限制非本地网络的流量转发速率,默认无限制,单位 BPS (字节每秒)" + instance_recv_bps_limit: + en: "the maximum total receive bps limit for this instance, default is no limit. unit: BPS (bytes per second)" + zh-CN: "限制当前网络实例整体入站流量的总接收速率,默认无限制,单位 BPS (字节每秒)" tcp_whitelist: en: "tcp port whitelist. Supports single ports (80) and ranges (8000-9000)" zh-CN: "TCP 端口白名单。支持单个端口(80)和范围(8000-9000)" diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index c071bd98..e4c9c5e6 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -69,6 +69,7 @@ pub fn gen_default_flags() -> Flags { quic_listen_port: u32::MAX, need_p2p: false, + instance_recv_bps_limit: u64::MAX, } } diff --git a/easytier/src/core.rs b/easytier/src/core.rs index 92f2f70b..ddfa1b35 100644 --- a/easytier/src/core.rs +++ b/easytier/src/core.rs @@ -560,6 +560,13 @@ struct NetworkOptions { )] foreign_relay_bps_limit: Option, + #[arg( + long, + env = "ET_INSTANCE_RECV_BPS_LIMIT", + help = t!("core_clap.instance_recv_bps_limit").to_string(), + )] + instance_recv_bps_limit: Option, + #[arg( long, value_delimiter = ',', @@ -1060,6 +1067,9 @@ impl NetworkOptions { f.foreign_relay_bps_limit = self .foreign_relay_bps_limit .unwrap_or(f.foreign_relay_bps_limit); + f.instance_recv_bps_limit = self + .instance_recv_bps_limit + .unwrap_or(f.instance_recv_bps_limit); f.multi_thread_count = self.multi_thread_count.unwrap_or(f.multi_thread_count); f.disable_relay_kcp = self.disable_relay_kcp.unwrap_or(f.disable_relay_kcp); f.disable_relay_quic = self.disable_relay_quic.unwrap_or(f.disable_relay_quic); diff --git a/easytier/src/launcher.rs b/easytier/src/launcher.rs index 85000810..ed77f55e 100644 --- a/easytier/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -826,6 +826,10 @@ impl NetworkConfig { flags.mtu = mtu as u32; } + if let Some(instance_recv_bps_limit) = self.instance_recv_bps_limit { + flags.instance_recv_bps_limit = instance_recv_bps_limit; + } + if let Some(enable_private_mode) = self.enable_private_mode { flags.private_mode = enable_private_mode; } @@ -978,6 +982,8 @@ impl NetworkConfig { result.disable_sym_hole_punching = Some(flags.disable_sym_hole_punching); result.enable_magic_dns = Some(flags.accept_dns); result.mtu = Some(flags.mtu as i32); + result.instance_recv_bps_limit = + (flags.instance_recv_bps_limit != u64::MAX).then_some(flags.instance_recv_bps_limit); result.enable_private_mode = Some(flags.private_mode); if flags.relay_network_whitelist == "*" { diff --git a/easytier/src/peers/peer_conn.rs b/easytier/src/peers/peer_conn.rs index ff22b470..32b710f7 100644 --- a/easytier/src/peers/peer_conn.rs +++ b/easytier/src/peers/peer_conn.rs @@ -1365,6 +1365,17 @@ impl PeerConn { &format!("{}:recv", conn_info_for_instrument.network_name), limiter_config.into(), )) + } else if self.global_ctx.get_flags().instance_recv_bps_limit != u64::MAX { + let limiter_config = LimiterConfig { + burst_rate: None, + bps: Some(self.global_ctx.get_flags().instance_recv_bps_limit), + fill_duration_ms: None, + }; + Some( + self.global_ctx + .token_bucket_manager() + .get_or_create("instance:recv", limiter_config.into()), + ) } else { None }; diff --git a/easytier/src/proto/api_manage.proto b/easytier/src/proto/api_manage.proto index 4d6ae2e1..b79ff5d7 100644 --- a/easytier/src/proto/api_manage.proto +++ b/easytier/src/proto/api_manage.proto @@ -87,6 +87,7 @@ message NetworkConfig { optional string credential_file = 57; optional bool lazy_p2p = 58; optional bool need_p2p = 59; + optional uint64 instance_recv_bps_limit = 60; } message PortForwardConfig { diff --git a/easytier/src/proto/common.proto b/easytier/src/proto/common.proto index 1497bfb2..26a4759a 100644 --- a/easytier/src/proto/common.proto +++ b/easytier/src/proto/common.proto @@ -73,6 +73,7 @@ message FlagsInConfig { bool lazy_p2p = 37; bool need_p2p = 38; + uint64 instance_recv_bps_limit = 39; } message RpcDescriptor { diff --git a/easytier/src/tests/three_node.rs b/easytier/src/tests/three_node.rs index 34067b56..36d70344 100644 --- a/easytier/src/tests/three_node.rs +++ b/easytier/src/tests/three_node.rs @@ -1535,6 +1535,48 @@ pub async fn relay_bps_limit_test(#[values(100, 200, 400, 800)] bps_limit: u64) drop_insts(insts).await; } +#[rstest::rstest] +#[serial_test::serial] +#[tokio::test] +pub async fn instance_recv_bps_limit_test(#[values(100, 800)] bps_limit: u64) { + let insts = init_three_node_ex( + "tcp", + |cfg| { + if cfg.get_inst_name() == "inst2" { + let mut f = cfg.get_flags(); + f.instance_recv_bps_limit = bps_limit * 1024; + cfg.set_flags(f); + } + cfg + }, + false, + ) + .await; + + let tcp_listener = TcpTunnelListener::new("tcp://0.0.0.0:22223".parse().unwrap()); + let tcp_connector = TcpTunnelConnector::new("tcp://10.144.144.3:22223".parse().unwrap()); + + let bps = _tunnel_bench_netns( + tcp_listener, + tcp_connector, + NetNS::new(Some("net_c".into())), + NetNS::new(Some("net_a".into())), + ) + .await; + + println!("bps: {}", bps); + + let bps = bps as u64 / 1024; + assert!( + bps >= bps_limit - 50 && bps <= bps_limit + 50, + "bps: {}, bps_limit: {}", + bps, + bps_limit + ); + + drop_insts(insts).await; +} + async fn assert_try_direct_connect_err(inst: &Instance, connector: C) where C: crate::tunnel::TunnelConnector + std::fmt::Debug,