Feat/pseudo dhcp (#109)

*  feat: pseudo dhcp
This commit is contained in:
m1m1sha 2024-05-17 23:16:56 +08:00 committed by GitHub
parent bad6a5946a
commit 0ead308392
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 314 additions and 113 deletions

View File

@ -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。

View File

@ -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.

View File

@ -41,6 +41,7 @@ impl Default for NetworkingMethod {
struct NetworkConfig {
instance_id: String,
dhcp: bool,
virtual_ipv4: String,
hostname: Option<String>,
network_name: String,
@ -53,7 +54,7 @@ struct NetworkConfig {
proxy_cidrs: Vec<String>,
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.dhcp {
if self.virtual_ipv4.len() > 0 {
cfg.set_ipv4(
self.virtual_ipv4.parse().with_context(|| {
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
)
})?,
});

View File

@ -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 () => {
<template>
<div class="flex flex-column h-full">
<div class="flex flex-column">
<div class="w-7/12 self-center ">
<Message severity="warn">
{{ $t('dhcp_experimental_warning') }}
</Message>
</div>
<div class="w-7/12 self-center ">
<Panel :header="$t('basic_settings')">
<div class="flex flex-column gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
<label for="virtual_ip">{{ $t('virtual_ipv4') }}</label>
<div class="flex align-items-center" for="virtual_ip">
<label class="mr-2"> {{ $t('virtual_ipv4') }} </label>
<Checkbox v-model="curNetwork.dhcp" input-id="virtual_ip_auto" :binary="true" />
<label for="virtual_ip_auto" class="ml-2">
{{ t('virtual_ipv4_dhcp') }}
</label>
</div>
<InputGroup>
<InputText id="virtual_ip" v-model="curNetwork.virtual_ipv4" aria-describedby="virtual_ipv4-help" />
<InputText
id="virtual_ip" v-model="curNetwork.virtual_ipv4" :disabled="curNetwork.dhcp"
aria-describedby="virtual_ipv4-help"
/>
<InputGroupAddon>
<span>/24</span>
</InputGroupAddon>
@ -112,7 +128,7 @@ onMounted(async () => {
<Divider />
<Panel :header="$t('advanced_settings')" toggleable>
<Panel :header="$t('advanced_settings')" toggleable collapsed>
<div class="flex flex-column gap-y-2">
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-column gap-2 basis-5/12 grow">
@ -154,7 +170,7 @@ onMounted(async () => {
</InputGroup>
</div>
<InputNumber
v-if="curNetwork.enable_vpn_portal" v-model="curNetwork.vpn_portal_listne_port"
v-if="curNetwork.enable_vpn_portal" v-model="curNetwork.vpn_portal_listen_port"
:placeholder="$t('vpn_portal_listen_port')" class="" :format="false" :min="0" :max="65535"
/>
</div>

View File

@ -24,7 +24,7 @@ const curNetworkInst = computed(() => {
const peerRouteInfos = computed(() => {
if (curNetworkInst.value)
return curNetworkInst.value.detail.peer_route_pairs
return curNetworkInst.value.detail?.peer_route_pairs || []
return []
})
@ -116,6 +116,13 @@ const myNodeInfoChips = computed(() => {
if (!my_node_info)
return chips
// virtual ipv4
chips.push({
label: `Virtual IPv4: ${my_node_info.virtual_ipv4}`,
icon: '',
} as Chip)
// local ipv4s
const local_ipv4s = my_node_info.ips?.interface_ipv4s
for (const [idx, ip] of local_ipv4s?.entries()) {
@ -290,7 +297,8 @@ function showEventLogs() {
</template>
</Card>
<Card v-if="!curNetworkInst?.error_msg">
<template v-else>
<Card>
<template #title>
{{ $t('my_node_info') }}
</template>
@ -351,7 +359,7 @@ function showEventLogs() {
<Divider />
<Card v-if="!curNetworkInst?.error_msg">
<Card>
<template #title>
{{ $t('peer_info') }}
</template>
@ -367,5 +375,6 @@ function showEventLogs() {
</DataTable>
</template>
</Card>
</template>
</div>
</template>

View File

@ -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) {
<div class="flex items-start content-center">
<div class="mr-3">
<span>{{ slotProps.value.network_name }}</span>
<span v-if="isRunning(slotProps.value.instance_id)" class="ml-3">
{{ slotProps.value.virtual_ipv4 }}
<span
v-if="isRunning(slotProps.value.instance_id) && networkStore.instances[slotProps.value.instance_id].detail && (networkStore.instances[slotProps.value.instance_id].detail?.my_node_info.virtual_ipv4 !== '')"
class="ml-3"
>
{{ networkStore.instances[slotProps.value.instance_id].detail
? networkStore.instances[slotProps.value.instance_id].detail?.my_node_info.virtual_ipv4 : '' }}
</span>
</div>
<Tag

View File

@ -56,7 +56,7 @@ export const useNetworkStore = defineStore('networkStore', {
instance_id: instanceId,
running: false,
error_msg: '',
detail: {} as NetworkInstanceRunningInfo,
detail: undefined,
}
},

View File

@ -31,3 +31,18 @@
border-radius: 10px;
margin-bottom: 1rem;
}
::-webkit-scrollbar {
width: 4px;
height: 4px;
border-radius: 4px;
}
::-webkit-scrollbar-track {
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: #0000005d;
}

View File

@ -9,6 +9,7 @@ export enum NetworkingMethod {
export interface NetworkConfig {
instance_id: string
dhcp: boolean
virtual_ipv4: string
hostname?: string
network_name: string
@ -17,18 +18,18 @@ export interface NetworkConfig {
networking_method: NetworkingMethod
public_server_url: string
peer_urls: Array<string>
peer_urls: string[]
proxy_cidrs: Array<string>
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<string>
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 {

View File

@ -24,7 +24,10 @@ pub trait ConfigLoader: Send + Sync {
fn set_netns(&self, ns: Option<String>);
fn get_ipv4(&self) -> Option<std::net::Ipv4Addr>;
fn set_ipv4(&self, addr: std::net::Ipv4Addr);
fn set_ipv4(&self, addr: Option<std::net::Ipv4Addr>);
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<String>,
instance_id: Option<uuid::Uuid>,
ipv4: Option<String>,
dhcp: Option<bool>,
network_identity: Option<NetworkIdentity>,
listeners: Option<Vec<url::Url>>,
@ -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<std::net::Ipv4Addr>) {
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) {

View File

@ -36,6 +36,9 @@ pub enum GlobalCtxEvent {
VpnPortalClientConnected(String, String), // (portal, client ip)
VpnPortalClientDisconnected(String, String), // (portal, client ip)
DhcpIpv4Changed(Option<std::net::Ipv4Addr>, Option<std::net::Ipv4Addr>), // (old, new)
DhcpIpv4Conflicted(Option<std::net::Ipv4Addr>),
}
type EventBus = tokio::sync::broadcast::Sender<GlobalCtxEvent>;
@ -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<std::net::Ipv4Addr>) {
self.config.set_ipv4(addr);
self.cached_ipv4.store(None);
}

View File

@ -71,6 +71,13 @@ struct Cli {
)]
ipv4: Option<String>,
#[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<String>,
@ -271,12 +278,16 @@ impl From<Cli> for TomlConfigLoader {
cli.network_secret.clone(),
));
cfg.set_dhcp(cli.dhcp);
if !cli.dhcp {
if let Some(ipv4) = &cli.ipv4 {
cfg.set_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));
}
}
}
});

View File

@ -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<Ipv4Inet> = None;
let mut tries = 6;
loop {
let mut ipv4_addr: Option<Ipv4Inet> = 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::<Ipv4Addr>() {
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();

View File

@ -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(),