From 959ccdfd9835ce85bafbae471cac07eb5ede19fb Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 22 Apr 2024 20:43:44 +0800 Subject: [PATCH] Reintroduce P2P Settings (#2365) * redo backend to be less cringe * fixed up --- core/src/api/mod.rs | 8 +- core/src/api/nodes.rs | 17 +- core/src/node/config.rs | 47 +++++- core/src/p2p/manager.rs | 52 +++--- .../$libraryId/settings/client/general.tsx | 156 ++++++++++-------- interface/locales/en/common.json | 3 + packages/client/src/core.ts | 8 +- 7 files changed, 166 insertions(+), 125 deletions(-) diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs index 753632694..aba4cdcfb 100644 --- a/core/src/api/mod.rs +++ b/core/src/api/mod.rs @@ -1,7 +1,7 @@ use crate::{ invalidate_query, node::{ - config::{NodeConfig, NodePreferences, P2PDiscoveryState, Port}, + config::{NodeConfig, NodeConfigP2P, NodePreferences, P2PDiscoveryState}, get_hardware_model_name, HardwareModel, }, old_job::JobProgressEvent, @@ -84,8 +84,7 @@ pub struct SanitisedNodeConfig { /// name is the display name of the current node. This is set by the user and is shown in the UI. // TODO: Length validation so it can fit in DNS record pub name: String, pub identity: RemoteIdentity, - pub p2p_ipv4_port: Port, - pub p2p_ipv6_port: Port, + pub p2p: NodeConfigP2P, pub p2p_discovery: P2PDiscoveryState, pub features: Vec, pub preferences: NodePreferences, @@ -98,8 +97,7 @@ impl From for SanitisedNodeConfig { id: value.id, name: value.name, identity: value.identity.to_remote_identity(), - p2p_ipv4_port: value.p2p_ipv4_port, - p2p_ipv6_port: value.p2p_ipv6_port, + p2p: value.p2p, p2p_discovery: value.p2p_discovery, features: value.features, preferences: value.preferences, diff --git a/core/src/api/nodes.rs b/core/src/api/nodes.rs index b77c71f4b..dd013a2fc 100644 --- a/core/src/api/nodes.rs +++ b/core/src/api/nodes.rs @@ -19,8 +19,9 @@ pub(crate) fn mount() -> AlphaRouter { #[derive(Deserialize, Type)] pub struct ChangeNodeNameArgs { pub name: Option, - pub p2p_ipv4_port: Option, - pub p2p_ipv6_port: Option, + pub p2p_port: Option, + pub p2p_ipv4_enabled: Option, + pub p2p_ipv6_enabled: Option, pub p2p_discovery: Option, pub image_labeler_version: Option, } @@ -43,14 +44,14 @@ pub(crate) fn mount() -> AlphaRouter { config.name = name; } - if let Some(port) = args.p2p_ipv4_port { - config.p2p_ipv4_port = port; + if let Some(port) = args.p2p_port { + config.p2p.port = port; }; - if let Some(port) = args.p2p_ipv6_port { - config.p2p_ipv6_port = port; + if let Some(enabled) = args.p2p_ipv4_enabled { + config.p2p.ipv4 = enabled; }; - if let Some(v) = args.p2p_discovery { - config.p2p_discovery = v; + if let Some(enabled) = args.p2p_ipv6_enabled { + config.p2p.ipv6 = enabled; }; #[cfg(feature = "ai")] diff --git a/core/src/node/config.rs b/core/src/node/config.rs index 4518a38f8..4441e06b8 100644 --- a/core/src/node/config.rs +++ b/core/src/node/config.rs @@ -37,20 +37,54 @@ pub enum P2PDiscoveryState { } #[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Type)] -#[serde(rename_all = "snake_case", untagged)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] pub enum Port { - Disabled, #[default] Random, Discrete(u16), } impl Port { + pub fn get(&self) -> u16 { + match self { + Port::Random => 0, + Port::Discrete(port) => *port, + } + } + pub fn is_random(&self) -> bool { matches!(self, Port::Random) } } +fn default_as_true() -> bool { + true +} + +fn skip_if_true(value: &bool) -> bool { + *value == true +} + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct NodeConfigP2P { + #[serde(default, skip_serializing_if = "Port::is_random")] + pub port: Port, + #[serde(default = "default_as_true", skip_serializing_if = "skip_if_true")] + pub ipv4: bool, + #[serde(default = "default_as_true", skip_serializing_if = "skip_if_true")] + pub ipv6: bool, +} + +impl Default for NodeConfigP2P { + fn default() -> Self { + Self { + port: Port::Random, + ipv4: true, + ipv6: true, + } + } +} + /// NodeConfig is the configuration for a node. This is shared between all libraries and is stored in a JSON file on disk. #[derive(Debug, Clone, Serialize, Deserialize)] // If you are adding `specta::Type` on this your probably about to leak the P2P private key pub struct NodeConfig { @@ -66,10 +100,8 @@ pub struct NodeConfig { #[serde(with = "identity_serde")] pub identity: Identity, /// P2P config - #[serde(default, skip_serializing_if = "Port::is_random")] - pub p2p_ipv4_port: Port, - #[serde(default, skip_serializing_if = "Port::is_random")] - pub p2p_ipv6_port: Port, + #[serde(default)] + pub p2p: NodeConfigP2P, #[serde(default)] pub p2p_discovery: P2PDiscoveryState, /// Feature flags enabled on the node @@ -153,8 +185,7 @@ impl ManagedVersion for NodeConfig { id: Uuid::new_v4(), name, identity: Identity::default(), - p2p_ipv4_port: Port::Random, - p2p_ipv6_port: Port::Random, + p2p: NodeConfigP2P::default(), p2p_discovery: P2PDiscoveryState::Everyone, version: Self::LATEST_VERSION, features: vec![], diff --git a/core/src/p2p/manager.rs b/core/src/p2p/manager.rs index 6794dc7b5..b692b83d9 100644 --- a/core/src/p2p/manager.rs +++ b/core/src/p2p/manager.rs @@ -1,6 +1,6 @@ use crate::{ node::{ - config::{self, P2PDiscoveryState, Port}, + config::{self, P2PDiscoveryState}, get_hardware_model_name, HardwareModel, }, p2p::{ @@ -146,18 +146,19 @@ impl P2PManager { } .update(&mut self.p2p.metadata_mut()); - let port = match config.p2p_ipv4_port { - Port::Disabled => None, - Port::Random => Some(0), - Port::Discrete(port) => Some(port), - }; - info!("Setting quic ipv4 listener to: {port:?}"); - if let Err(err) = self.quic.set_ipv4_enabled(port).await { + let port = config.p2p.port.get(); + + info!( + "Setting quic ipv4 listener to: {:?}", + config.p2p.ipv4.then(|| port) + ); + if let Err(err) = self + .quic + .set_ipv4_enabled(config.p2p.ipv4.then(|| port)) + .await + { error!("Failed to enabled quic ipv4 listener: {err}"); - self.node_config - .write(|c| c.p2p_ipv4_port = Port::Disabled) - .await - .ok(); + self.node_config.write(|c| c.p2p.ipv4 = false).await.ok(); self.listener_errors .lock() @@ -165,18 +166,17 @@ impl P2PManager { .ipv4 = Some(format!("{err}")); } - let port = match config.p2p_ipv6_port { - Port::Disabled => None, - Port::Random => Some(0), - Port::Discrete(port) => Some(port), - }; - info!("Setting quic ipv6 listener to: {port:?}"); - if let Err(err) = self.quic.set_ipv6_enabled(port).await { + info!( + "Setting quic ipv6 listener to: {:?}", + config.p2p.ipv6.then(|| port) + ); + if let Err(err) = self + .quic + .set_ipv6_enabled(config.p2p.ipv6.then(|| port)) + .await + { error!("Failed to enabled quic ipv6 listener: {err}"); - self.node_config - .write(|c| c.p2p_ipv6_port = Port::Disabled) - .await - .ok(); + self.node_config.write(|c| c.p2p.ipv6 = false).await.ok(); self.listener_errors .lock() @@ -270,11 +270,7 @@ impl P2PManager { "name": name, "listener_addrs": listeners.iter().find(|l| l.is_hook_id(*id)).map(|l| l.addrs.clone()), })).collect::>(), - "config": json!({ - "p2p_ipv4_port": node_config.p2p_ipv4_port, - "p2p_ipv6_port": node_config.p2p_ipv6_port, - "p2p_discovery": node_config.p2p_discovery, - }), + "config": node_config.p2p, "relay_config": self.quic.get_relay_config(), }) } diff --git a/interface/app/$libraryId/settings/client/general.tsx b/interface/app/$libraryId/settings/client/general.tsx index 9ba51256b..25d1b6d36 100644 --- a/interface/app/$libraryId/settings/client/general.tsx +++ b/interface/app/$libraryId/settings/client/general.tsx @@ -1,4 +1,5 @@ -import { FormProvider } from 'react-hook-form'; +import clsx from 'clsx'; +import { Controller, FormProvider } from 'react-hook-form'; import { useBridgeMutation, useBridgeQuery, @@ -37,6 +38,8 @@ const LANGUAGE_OPTIONS = [ // Sort the languages by their label LANGUAGE_OPTIONS.sort((a, b) => a.label.localeCompare(b.label)); +const u16 = () => z.number().min(0).max(65535); + export const Component = () => { const node = useBridgeQuery(['nodeState']); const platform = usePlatform(); @@ -50,9 +53,12 @@ export const Component = () => { schema: z .object({ name: z.string().min(1).max(250).optional(), - // p2p_enabled: z.boolean().optional(), - // p2p_port: u16, - // customOrDefault: z.enum(['Custom', 'Default']), + p2p_port: z.discriminatedUnion('type', [ + z.object({ type: z.literal('random') }), + z.object({ type: z.literal('discrete'), value: u16() }) + ]), + p2p_ipv4_enabled: z.boolean().optional(), + p2p_ipv6_enabled: z.boolean().optional(), image_labeler_version: z.string().optional(), background_processing_percentage: z.coerce .number({ @@ -66,25 +72,26 @@ export const Component = () => { reValidateMode: 'onChange', defaultValues: { name: node.data?.name, - // p2p_port: node.data?.p2p_port || 0, - // p2p_enabled: node.data?.p2p_enabled, - // customOrDefault: node.data?.p2p_port ? 'Custom' : 'Default', + p2p_port: node.data?.p2p.port || { type: 'random' }, + p2p_ipv4_enabled: node.data?.p2p.ipv4 || true, + p2p_ipv6_enabled: node.data?.p2p.ipv6 || true, image_labeler_version: node.data?.image_labeler_version ?? undefined, background_processing_percentage: node.data?.preferences.thumbnailer.background_processing_percentage || 50 } }); + const p2p_port = form.watch('p2p_port'); - // const watchCustomOrDefault = form.watch('customOrDefault'); - // const watchP2pEnabled = form.watch('p2p_enabled'); const watchBackgroundProcessingPercentage = form.watch('background_processing_percentage'); useDebouncedFormWatch(form, async (value) => { if (await form.trigger()) { await editNode.mutateAsync({ name: value.name || null, - p2p_ipv4_port: null, - p2p_ipv6_port: null, + + p2p_port: (value.p2p_port as any) ?? null, + p2p_ipv4_enabled: value.p2p_ipv4_enabled ?? null, + p2p_ipv6_enabled: value.p2p_ipv6_enabled ?? null, p2p_discovery: null, // p2p_port: value.customOrDefault === 'Default' ? 0 : Number(value.p2p_port), // p2p_enabled: value.p2p_enabled ?? null, @@ -101,11 +108,11 @@ export const Component = () => { node.refetch(); }); - // form.watch((data) => { - // if (Number(data.p2p_port) > 65535) { - // form.setValue('p2p_port', 65535); - // } - // }); + form.watch((data) => { + if (data.p2p_port?.type == 'discrete' && Number(data.p2p_port.value) > 65535) { + form.setValue('p2p_port', { type: 'discrete', value: 65535 }); + } + }); const { t } = useLocale(); @@ -288,20 +295,10 @@ export const Component = () => { /> */} - {/*
-

{t('networking')}

*/} +
+

{t('networking')}

- {/* TODO: Add some UI for this stuff */} - {/* {node.data?.p2p.ipv4.status === 'Listening' || - node.data?.p2p.ipv4.status === 'Enabling' - ? `0.0.0.0:${node.data?.p2p.ipv4?.port || 0}` - : ''} - {node.data?.p2p.ipv6.status === 'Listening' || - node.data?.p2p.ipv6.status === 'Enabling' - ? `[::1]:${node.data?.p2p.ipv6?.port || 0}` - : ''} */} - - {/* { > form.setValue('p2p_enabled', !form.getValues('p2p_enabled'))} - // disabled - onClick={() => toast.info(t('coming_soon'))} + checked={form.watch('p2p_ipv4_enabled') && form.watch('p2p_ipv6_enabled')} + onCheckedChange={(checked) => { + form.setValue('p2p_ipv4_enabled', checked); + form.setValue('p2p_ipv6_enabled', checked); + }} /> - */} - {/* -
- ( + + + {form.watch('p2p_ipv4_enabled') && form.watch('p2p_ipv6_enabled') ? ( + <> + +
- )} - /> - { - form.setValue( - 'p2p_port', - Number(e.target.value.replace(/[^0-9]/g, '')) - ); - }} - /> -
-
*/} - {/*
*/} + { + form.setValue('p2p_port', { + type: 'discrete', + value: Number(e.target.value.replace(/[^0-9]/g, '')) + }); + }} + /> +
+ + {t('ipv6_description')}

+ } + > + + form.setValue('p2p_ipv6_enabled', checked) + } + /> +
+ + ) : null} +
); }; diff --git a/interface/locales/en/common.json b/interface/locales/en/common.json index 30bde6dce..212ddd23f 100644 --- a/interface/locales/en/common.json +++ b/interface/locales/en/common.json @@ -94,6 +94,9 @@ "debug_mode": "Debug mode", "debug_mode_description": "Enable extra debugging features within the app.", "default": "Default", + "random": "Random", + "ipv6": "IPv6 networking", + "ipv6_description": "Allow peer-to-peer communication using IPv6 networking", "default_settings": "Default Settings", "delete": "Delete", "delete_dialog_title": "Delete {{prefix}} {{type}}", diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 1a2c2ab79..3da3a6c63 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -169,7 +169,7 @@ export type CacheNode = { __type: string; __id: string; "#node": any } export type CameraData = { device_make: string | null; device_model: string | null; color_space: string | null; color_profile: ColorProfile | null; focal_length: number | null; shutter_speed: number | null; flash: Flash | null; orientation: Orientation; lens_make: string | null; lens_model: string | null; bit_depth: number | null; red_eye: boolean | null; zoom: number | null; iso: number | null; software: string | null; serial_number: string | null; lens_serial_number: string | null; contrast: number | null; saturation: number | null; sharpness: number | null; composite: Composite | null } -export type ChangeNodeNameArgs = { name: string | null; p2p_ipv4_port: Port | null; p2p_ipv6_port: Port | null; p2p_discovery: P2PDiscoveryState | null; image_labeler_version: string | null } +export type ChangeNodeNameArgs = { name: string | null; p2p_port: Port | null; p2p_ipv4_enabled: boolean | null; p2p_ipv6_enabled: boolean | null; p2p_discovery: P2PDiscoveryState | null; image_labeler_version: string | null } export type CloudInstance = { id: string; uuid: string; identity: RemoteIdentity; nodeId: string; metadata: { [key in string]: string } } @@ -450,6 +450,8 @@ export type MediaLocation = { latitude: number; longitude: number; pluscode: Plu export type MediaMetadata = ({ type: "Image" } & ImageMetadata) | ({ type: "Video" } & VideoMetadata) | ({ type: "Audio" } & AudioMetadata) +export type NodeConfigP2P = { port: Port; ipv4: boolean; ipv6: boolean } + export type NodePreferences = { thumbnailer: ThumbnailerPreferences } export type NodeState = ({ @@ -460,7 +462,7 @@ id: string; /** * name is the display name of the current node. This is set by the user and is shown in the UI. // TODO: Length validation so it can fit in DNS record */ -name: string; identity: RemoteIdentity; p2p_ipv4_port: Port; p2p_ipv6_port: Port; p2p_discovery: P2PDiscoveryState; features: BackendFeature[]; preferences: NodePreferences; image_labeler_version: string | null }) & { data_path: string; device_model: string | null } +name: string; identity: RemoteIdentity; p2p: NodeConfigP2P; p2p_discovery: P2PDiscoveryState; features: BackendFeature[]; preferences: NodePreferences; image_labeler_version: string | null }) & { data_path: string; device_model: string | null } export type NonIndexedPathItem = { path: string; name: string; extension: string; kind: number; is_dir: boolean; date_created: string; date_modified: string; size_in_bytes_bytes: number[]; hidden: boolean } @@ -539,7 +541,7 @@ export type PeerMetadata = { name: string; operating_system: OperatingSystem | n export type PlusCode = string -export type Port = null | number +export type Port = { type: "random" } | { type: "discrete"; value: number } export type Range = { from: T } | { to: T }