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,