diff --git a/easytier-gui/locales/cn.yml b/easytier-gui/locales/cn.yml index bc29c5e..17070b1 100644 --- a/easytier-gui/locales/cn.yml +++ b/easytier-gui/locales/cn.yml @@ -4,6 +4,7 @@ public_server: 公共服务器 manual: 手动 standalone: 独立 virtual_ipv4: 虚拟IPv4地址 +virtual_ipv4_dhcp: DHCP network_name: 网络名称 network_secret: 网络密码 public_server_url: 公共服务器地址 @@ -59,3 +60,4 @@ run_network: 运行网络 stop_network: 停止网络 network_running: 运行中 network_stopped: 已停止 +dhcp_experimental_warning: 实验性警告!使用DHCP时如果组网环境中发生IP冲突,将自动更改IP。 diff --git a/easytier-gui/locales/en.yml b/easytier-gui/locales/en.yml index 4321cb4..72ac2b4 100644 --- a/easytier-gui/locales/en.yml +++ b/easytier-gui/locales/en.yml @@ -4,6 +4,7 @@ public_server: Public Server manual: Manual standalone: Standalone virtual_ipv4: Virtual IPv4 +virtual_ipv4_dhcp: DHCP network_name: Network Name network_secret: Network Secret public_server_url: Public Server URL @@ -59,3 +60,4 @@ run_network: Run Network stop_network: Stop Network network_running: running network_stopped: stopped +dhcp_experimental_warning: Experimental warning! if there is an IP conflict in the network when using DHCP, the IP will be automatically changed. diff --git a/easytier-gui/src-tauri/src/main.rs b/easytier-gui/src-tauri/src/main.rs index 867bfe1..be19eca 100644 --- a/easytier-gui/src-tauri/src/main.rs +++ b/easytier-gui/src-tauri/src/main.rs @@ -41,6 +41,7 @@ impl Default for NetworkingMethod { struct NetworkConfig { instance_id: String, + dhcp: bool, virtual_ipv4: String, hostname: Option, network_name: String, @@ -53,7 +54,7 @@ struct NetworkConfig { proxy_cidrs: Vec, enable_vpn_portal: bool, - vpn_portal_listne_port: i32, + vpn_portal_listen_port: i32, vpn_portal_client_network_addr: String, vpn_portal_client_network_len: i32, @@ -72,18 +73,19 @@ impl NetworkConfig { .with_context(|| format!("failed to parse instance id: {}", self.instance_id))?, ); cfg.set_hostname(self.hostname.clone()); + cfg.set_dhcp(self.dhcp); cfg.set_inst_name(self.network_name.clone()); cfg.set_network_identity(NetworkIdentity::new( self.network_name.clone(), self.network_secret.clone(), )); - if self.virtual_ipv4.len() > 0 { - cfg.set_ipv4( - self.virtual_ipv4.parse().with_context(|| { + if !self.dhcp { + if self.virtual_ipv4.len() > 0 { + cfg.set_ipv4(Some(self.virtual_ipv4.parse().with_context(|| { format!("failed to parse ipv4 address: {}", self.virtual_ipv4) - })?, - ) + })?)) + } } match self.networking_method { @@ -150,12 +152,12 @@ impl NetworkConfig { client_cidr: cidr .parse() .with_context(|| format!("failed to parse vpn portal client cidr: {}", cidr))?, - wireguard_listen: format!("0.0.0.0:{}", self.vpn_portal_listne_port) + wireguard_listen: format!("0.0.0.0:{}", self.vpn_portal_listen_port) .parse() .with_context(|| { format!( "failed to parse vpn portal wireguard listen port. {}", - self.vpn_portal_listne_port + self.vpn_portal_listen_port ) })?, }); diff --git a/easytier-gui/src/components/Config.vue b/easytier-gui/src/components/Config.vue index bdc939a..46af71c 100644 --- a/easytier-gui/src/components/Config.vue +++ b/easytier-gui/src/components/Config.vue @@ -2,7 +2,6 @@ import InputGroup from 'primevue/inputgroup' import InputGroupAddon from 'primevue/inputgroupaddon' import { getOsHostname } from '~/composables/network' -import { i18n } from '~/modules/i18n' import { NetworkingMethod } from '~/types/network' const props = defineProps<{ @@ -12,10 +11,12 @@ const props = defineProps<{ defineEmits(['runNetwork']) +const { t } = useI18n() + const networking_methods = ref([ - { value: NetworkingMethod.PublicServer, label: i18n.global.t('public_server') }, - { value: NetworkingMethod.Manual, label: i18n.global.t('manual') }, - { value: NetworkingMethod.Standalone, label: i18n.global.t('standalone') }, + { value: NetworkingMethod.PublicServer, label: t('public_server') }, + { value: NetworkingMethod.Manual, label: t('manual') }, + { value: NetworkingMethod.Standalone, label: t('standalone') }, ]) const networkStore = useNetworkStore() @@ -56,14 +57,29 @@ onMounted(async () => { - - - diff --git a/easytier-gui/src/pages/index.vue b/easytier-gui/src/pages/index.vue index 5414172..1bf1cbb 100644 --- a/easytier-gui/src/pages/index.vue +++ b/easytier-gui/src/pages/index.vue @@ -52,7 +52,6 @@ enum Severity { const messageBarSeverity = ref(Severity.None) const messageBarContent = ref('') - const toast = useToast() const networkStore = useNetworkStore() @@ -108,12 +107,8 @@ onMounted(() => { }) onUnmounted(() => clearInterval(intervalId)) -const curNetworkHasInstance = computed(() => { - return networkStore.networkInstanceIds.includes(networkStore.curNetworkId) -}) - const activeStep = computed(() => { - return curNetworkHasInstance.value ? 1 : 0 + return networkStore.networkInstanceIds.includes(networkStore.curNetworkId) ? 1 : 0 }) const setting_menu = ref() @@ -190,8 +185,12 @@ function isRunning(id: string) {
{{ slotProps.value.network_name }} - - {{ slotProps.value.virtual_ipv4 }} + + {{ networkStore.instances[slotProps.value.instance_id].detail + ? networkStore.instances[slotProps.value.instance_id].detail?.my_node_info.virtual_ipv4 : '' }}
+ peer_urls: string[] - proxy_cidrs: Array + proxy_cidrs: string[] enable_vpn_portal: boolean - vpn_portal_listne_port: number + vpn_portal_listen_port: number vpn_portal_client_network_addr: string vpn_portal_client_network_len: number advanced_settings: boolean - listener_urls: Array + listener_urls: string[] rpc_port: number } @@ -36,8 +37,9 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { return { instance_id: uuidv4(), + dhcp: false, virtual_ipv4: '', - network_name: 'default', + network_name: 'easytier', network_secret: '', networking_method: NetworkingMethod.PublicServer, @@ -48,7 +50,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { proxy_cidrs: [], enable_vpn_portal: false, - vpn_portal_listne_port: 22022, + vpn_portal_listen_port: 22022, vpn_portal_client_network_addr: '', vpn_portal_client_network_len: 24, @@ -59,7 +61,7 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig { 'udp://0.0.0.0:11010', 'wg://0.0.0.0:11011', ], - rpc_port: 15888, + rpc_port: 0, } } @@ -69,7 +71,7 @@ export interface NetworkInstance { running: boolean error_msg: string - detail: NetworkInstanceRunningInfo + detail?: NetworkInstanceRunningInfo } export interface NetworkInstanceRunningInfo { diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index 78f05c1..5d28446 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -24,7 +24,10 @@ pub trait ConfigLoader: Send + Sync { fn set_netns(&self, ns: Option); fn get_ipv4(&self) -> Option; - fn set_ipv4(&self, addr: std::net::Ipv4Addr); + fn set_ipv4(&self, addr: Option); + + fn get_dhcp(&self) -> bool; + fn set_dhcp(&self, dhcp: bool); fn add_proxy_cidr(&self, cidr: cidr::IpCidr); fn remove_proxy_cidr(&self, cidr: cidr::IpCidr); @@ -161,6 +164,7 @@ struct Config { instance_name: Option, instance_id: Option, ipv4: Option, + dhcp: Option, network_identity: Option, listeners: Option>, @@ -280,8 +284,20 @@ impl ConfigLoader for TomlConfigLoader { .flatten() } - fn set_ipv4(&self, addr: std::net::Ipv4Addr) { - self.config.lock().unwrap().ipv4 = Some(addr.to_string()); + fn set_ipv4(&self, addr: Option) { + self.config.lock().unwrap().ipv4 = if let Some(addr) = addr { + Some(addr.to_string()) + } else { + None + }; + } + + fn get_dhcp(&self) -> bool { + self.config.lock().unwrap().dhcp.unwrap_or_default() + } + + fn set_dhcp(&self, dhcp: bool) { + self.config.lock().unwrap().dhcp = Some(dhcp); } fn add_proxy_cidr(&self, cidr: cidr::IpCidr) { diff --git a/easytier/src/common/global_ctx.rs b/easytier/src/common/global_ctx.rs index 3e5c5d5..40c816f 100644 --- a/easytier/src/common/global_ctx.rs +++ b/easytier/src/common/global_ctx.rs @@ -36,6 +36,9 @@ pub enum GlobalCtxEvent { VpnPortalClientConnected(String, String), // (portal, client ip) VpnPortalClientDisconnected(String, String), // (portal, client ip) + + DhcpIpv4Changed(Option, Option), // (old, new) + DhcpIpv4Conflicted(Option), } type EventBus = tokio::sync::broadcast::Sender; @@ -127,7 +130,7 @@ impl GlobalCtx { return addr; } - pub fn set_ipv4(&mut self, addr: std::net::Ipv4Addr) { + pub fn set_ipv4(&self, addr: Option) { self.config.set_ipv4(addr); self.cached_ipv4.store(None); } diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs index 4eabed8..a0ccbbb 100644 --- a/easytier/src/easytier-core.rs +++ b/easytier/src/easytier-core.rs @@ -71,6 +71,13 @@ struct Cli { )] ipv4: Option, + #[arg( + short, + long, + help = "automatically determine and set IP address by Easytier, and the IP address starts from 10.0.0.1 by default. Warning, if there is an IP conflict in the network when using DHCP, the IP will be automatically changed." + )] + dhcp: bool, + #[arg(short, long, help = "peers to connect initially", num_args = 0..)] peers: Vec, @@ -271,12 +278,16 @@ impl From for TomlConfigLoader { cli.network_secret.clone(), )); - if let Some(ipv4) = &cli.ipv4 { - cfg.set_ipv4( - ipv4.parse() - .with_context(|| format!("failed to parse ipv4 address: {}", ipv4)) - .unwrap(), - ) + cfg.set_dhcp(cli.dhcp); + + if !cli.dhcp { + if let Some(ipv4) = &cli.ipv4 { + cfg.set_ipv4(Some( + ipv4.parse() + .with_context(|| format!("failed to parse ipv4 address: {}", ipv4)) + .unwrap(), + )) + } } cfg.set_peers( @@ -503,6 +514,14 @@ pub async fn async_main(cli: Cli) { portal, client_addr )); } + + GlobalCtxEvent::DhcpIpv4Changed(old, new) => { + print_event(format!("dhcp ip changed. old: {:?}, new: {:?}", old, new)); + } + + GlobalCtxEvent::DhcpIpv4Conflicted(ip) => { + print_event(format!("dhcp ip conflict. ip: {:?}", ip)); + } } } }); diff --git a/easytier/src/instance/instance.rs b/easytier/src/instance/instance.rs index 7ad1e91..025abd5 100644 --- a/easytier/src/instance/instance.rs +++ b/easytier/src/instance/instance.rs @@ -1,10 +1,12 @@ use std::borrow::BorrowMut; +use std::collections::HashSet; use std::net::Ipv4Addr; use std::pin::Pin; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; use anyhow::Context; +use cidr::Ipv4Inet; use futures::{SinkExt, StreamExt}; use pnet::packet::ipv4::Ipv4Packet; @@ -285,6 +287,116 @@ impl Instance { Ok(()) } + // Warning, if there is an IP conflict in the network when using DHCP, the IP will be automatically changed. + fn check_dhcp_ip_conflict(&self) { + use rand::Rng; + let peer_manager_c = self.peer_manager.clone(); + let global_ctx_c = self.get_global_ctx(); + let nic_c = self.virtual_nic.as_ref().unwrap().clone(); + tokio::spawn(async move { + let default_ipv4_addr = Ipv4Addr::new(10, 0, 0, 0); + let mut dhcp_ip: Option = None; + let mut tries = 6; + loop { + let mut ipv4_addr: Option = None; + let mut unique_ipv4 = HashSet::new(); + + for i in 0..tries { + if dhcp_ip.is_none() { + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + } + + for route in peer_manager_c.list_routes().await { + if !route.ipv4_addr.is_empty() { + if let Ok(ip) = Ipv4Inet::new( + if let Ok(ipv4) = route.ipv4_addr.parse::() { + ipv4 + } else { + default_ipv4_addr + }, + 24, + ) { + unique_ipv4.insert(ip); + } + } + } + + if i == tries - 1 && unique_ipv4.is_empty() { + unique_ipv4.insert(Ipv4Inet::new(default_ipv4_addr, 24).unwrap()); + } + + if let Some(ip) = dhcp_ip { + if !unique_ipv4.contains(&ip) { + ipv4_addr = dhcp_ip; + break; + } + } + + for net in unique_ipv4.iter().map(|inet| inet.network()).take(1) { + if let Some(ip) = net.iter().find(|ip| { + ip.address() != net.first_address() + && ip.address() != net.last_address() + && !unique_ipv4.contains(ip) + }) { + ipv4_addr = Some(ip); + } + } + } + + if dhcp_ip != ipv4_addr { + let last_ip = dhcp_ip.map(|p| p.address()); + tracing::debug!("last_ip: {:?}", last_ip); + let _ = nic_c.remove_ip(last_ip).await; + #[cfg(target_os = "macos")] + if last_ip.is_some() { + let _g = global_ctx_c.net_ns.guard(); + let ret = nic_c + .get_ifcfg() + .remove_ipv4_route(nic_c.ifname(), last_ip.unwrap(), 24) + .await; + + if ret.is_err() { + tracing::trace!( + cidr = 24, + err = ?ret, + "remove route failed.", + ); + } + } + + if let Some(ip) = ipv4_addr { + let _ = nic_c.link_up().await; + dhcp_ip = Some(ip); + tries = 1; + if let Err(e) = nic_c.add_ip(ip.address(), 24).await { + tracing::error!("add ip failed: {:?}", e); + global_ctx_c.set_ipv4(None); + let sleep: u64 = rand::thread_rng().gen_range(200..500); + tokio::time::sleep(std::time::Duration::from_millis(sleep)).await; + continue; + } + #[cfg(target_os = "macos")] + let _ = nic_c.add_route(ip.address(), 24).await; + global_ctx_c.set_ipv4(Some(ip.address())); + global_ctx_c.issue_event(GlobalCtxEvent::DhcpIpv4Changed( + last_ip, + Some(ip.address()), + )); + } else { + global_ctx_c.set_ipv4(None); + global_ctx_c.issue_event(GlobalCtxEvent::DhcpIpv4Conflicted(last_ip)); + dhcp_ip = None; + tries = 6; + } + } + + let sleep: u64 = rand::thread_rng().gen_range(5..10); + + tokio::time::sleep(std::time::Duration::from_secs(sleep)).await; + } + }); + } + pub async fn run(&mut self) -> Result<(), Error> { self.listener_manager .lock() @@ -294,7 +406,11 @@ impl Instance { self.listener_manager.lock().await.run().await?; self.peer_manager.run().await?; - if let Some(ipv4_addr) = self.global_ctx.get_ipv4() { + if self.global_ctx.config.get_dhcp() { + self.prepare_tun_device().await?; + self.run_proxy_cidrs_route_updater(); + self.check_dhcp_ip_conflict(); + } else if let Some(ipv4_addr) = self.global_ctx.get_ipv4() { self.prepare_tun_device().await?; self.assign_ipv4_to_tun_device(ipv4_addr).await?; self.run_proxy_cidrs_route_updater(); diff --git a/easytier/src/tests/three_node.rs b/easytier/src/tests/three_node.rs index 279b166..04c88b2 100644 --- a/easytier/src/tests/three_node.rs +++ b/easytier/src/tests/three_node.rs @@ -48,7 +48,7 @@ pub fn get_inst_config(inst_name: &str, ns: Option<&str>, ipv4: &str) -> TomlCon let config = TomlConfigLoader::default(); config.set_inst_name(inst_name.to_owned()); config.set_netns(ns.map(|s| s.to_owned())); - config.set_ipv4(ipv4.parse().unwrap()); + config.set_ipv4(Some(ipv4.parse().unwrap())); config.set_listeners(vec![ "tcp://0.0.0.0:11010".parse().unwrap(), "udp://0.0.0.0:11010".parse().unwrap(),