summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2025-07-17 18:24:56 +0200
committerDavid Lönnhager <david.l@mullvad.net>2025-07-23 09:41:54 +0200
commit38b8388e06b0fd60c3e4e6aa351eb541329dd3dd (patch)
tree0082708fb62cadfe9f1fd405c6ec7ac9ec75556d
parentbb4b111acc09a92fdf12e0fc6d504180f91421c8 (diff)
downloadmullvadvpn-38b8388e06b0fd60c3e4e6aa351eb541329dd3dd.tar.xz
mullvadvpn-38b8388e06b0fd60c3e4e6aa351eb541329dd3dd.zip
Refactor Relay protobuf type
Remove the dependency on google/protobuf/any.proto.
-rw-r--r--desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts53
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts2
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts4
-rw-r--r--desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts9
-rw-r--r--mullvad-api/src/relay_list.rs74
-rw-r--r--mullvad-management-interface/proto/management_interface.proto45
-rw-r--r--mullvad-management-interface/src/types/conversions/mod.rs19
-rw-r--r--mullvad-management-interface/src/types/conversions/relay_list.rs196
-rw-r--r--mullvad-relay-selector/src/relay_selector/helpers.rs33
-rw-r--r--mullvad-relay-selector/src/relay_selector/matcher.rs2
-rw-r--r--mullvad-relay-selector/tests/relay_selector.rs130
-rw-r--r--mullvad-types/src/relay_list.rs128
12 files changed, 302 insertions, 393 deletions
diff --git a/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts b/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts
index 1a49dae202..4b79b0b1d9 100644
--- a/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts
+++ b/desktop/packages/mullvad-vpn/src/main/grpc-type-convertions.ts
@@ -25,7 +25,6 @@ import {
ErrorStateCause,
ErrorStateDetails,
FeatureIndicator,
- FeaturesType,
FirewallPolicyError,
FirewallPolicyErrorType,
IAppVersionInfo,
@@ -53,7 +52,7 @@ import {
ObfuscationType,
Ownership,
ProxyType,
- RelayEndpointType,
+ Quic,
RelayLocation,
RelayLocationGeographical,
RelayProtocol,
@@ -119,43 +118,35 @@ function convertFromRelayListCity(city: grpcTypes.RelayListCity): IRelayListCity
function convertFromRelayListRelay(relay: grpcTypes.Relay): IRelayListHostname {
const relayObject = relay.toObject();
- let daita = false;
- if (relayObject.endpointType === grpcTypes.Relay.RelayType.WIREGUARD) {
- const endpointDataU8 = relay.getEndpointData()?.getValue_asU8();
- if (endpointDataU8) {
- daita = grpcTypes.WireguardRelayEndpointData.deserializeBinary(endpointDataU8).getDaita();
- }
- }
+ // The relay type is determined by the variant of the extra endpoint data
+ const wireguard = relayObject.endpointData?.wireguard;
+ const openvpn = relayObject.endpointData?.openvpn;
+ const bridge = relayObject.endpointData?.bridge;
+
+ const endpointType = wireguard
+ ? 'wireguard'
+ : openvpn
+ ? 'openvpn'
+ : bridge
+ ? 'bridge'
+ : /*This case should never happen ..*/ 'bridge';
+
+ const daita = wireguard ? wireguard.daita : false;
+ const quic = wireguard?.quic ? quicFromRelayType(wireguard.quic) : undefined;
return {
...relayObject,
- endpointType: convertFromRelayType(relayObject.endpointType),
- features: relayObject.features ? featuresFromRelayType(relayObject.features) : undefined,
+ endpointType,
daita,
+ quic,
};
}
-function convertFromRelayType(relayType: grpcTypes.Relay.RelayType): RelayEndpointType {
- const protocolMap: Record<grpcTypes.Relay.RelayType, RelayEndpointType> = {
- [grpcTypes.Relay.RelayType.OPENVPN]: 'openvpn',
- [grpcTypes.Relay.RelayType.BRIDGE]: 'bridge',
- [grpcTypes.Relay.RelayType.WIREGUARD]: 'wireguard',
- };
- return protocolMap[relayType];
-}
-
-function featuresFromRelayType(features: grpcTypes.Relay.Features.AsObject): FeaturesType {
- const daita = features.daita;
- const quic = features.quic
- ? {
- domain: features.quic.domain,
- token: features.quic.token,
- addr_in: features.quic.addrInList,
- }
- : undefined;
+function quicFromRelayType(quic: grpcTypes.Relay.RelayData.Wireguard.Quic.AsObject): Quic {
return {
- daita,
- quic,
+ domain: quic.domain,
+ token: quic.token,
+ addrIn: quic.addrInList,
};
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts
index fcfd3a38da..29ce00f1ff 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/filter-locations.ts
@@ -38,7 +38,7 @@ export function filterLocationsByQuic(
tunnelProtocol: TunnelProtocol,
): IRelayLocationCountryRedux[] {
const quicFilterActive = quic && tunnelProtocol !== 'openvpn';
- const quickOnRelay = (relay: IRelayLocationRelayRedux) => relay.features?.quic !== undefined;
+ const quickOnRelay = (relay: IRelayLocationRelayRedux) => relay.quic !== undefined;
return quicFilterActive ? filterLocationsImpl(locations, quickOnRelay) : locations;
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts b/desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts
index 9fc8a6d30e..9ac8bb3a61 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/redux/settings/reducers.ts
@@ -7,7 +7,6 @@ import {
BridgeType,
CustomLists,
CustomProxy,
- FeaturesType,
IDaitaSettings,
IDnsOptions,
IpVersion,
@@ -16,6 +15,7 @@ import {
ObfuscationSettings,
ObfuscationType,
Ownership,
+ Quic,
RelayEndpointType,
RelayLocation,
RelayOverride,
@@ -78,7 +78,7 @@ export interface IRelayLocationRelayRedux {
weight: number;
endpointType: RelayEndpointType;
daita: boolean;
- features?: FeaturesType;
+ quic?: Quic;
}
export interface IRelayLocationCityRedux {
diff --git a/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts b/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts
index f12c3123f8..49164a3770 100644
--- a/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts
+++ b/desktop/packages/mullvad-vpn/src/shared/daemon-rpc-types.ts
@@ -396,15 +396,10 @@ export interface IRelayListHostname {
weight: number;
owned: boolean;
endpointType: RelayEndpointType;
- daita: boolean; // TODO: Deprecate in favor of Features 👇
- features?: FeaturesType;
-}
-
-// The absence of a value signals that the relay does not have it enabled.
-export type FeaturesType = {
daita: boolean;
+ // The absence of this value signals that the relay does not deploy QUIC.
quic?: Quic;
-};
+}
export type Quic = {
domain: string;
diff --git a/mullvad-api/src/relay_list.rs b/mullvad-api/src/relay_list.rs
index abca6dd2b7..4a2d68a071 100644
--- a/mullvad-api/src/relay_list.rs
+++ b/mullvad-api/src/relay_list.rs
@@ -7,7 +7,7 @@ use mullvad_types::{location, relay_list};
use talpid_types::net::wireguard;
use std::{
- collections::BTreeMap,
+ collections::{BTreeMap, HashSet},
future::Future,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
ops::RangeInclusive,
@@ -167,7 +167,6 @@ fn into_mullvad_relay(
relay: Relay,
location: location::Location,
endpoint_data: relay_list::RelayEndpointData,
- features: relay_list::Features,
) -> relay_list::Relay {
relay_list::Relay {
hostname: relay.hostname,
@@ -182,7 +181,6 @@ fn into_mullvad_relay(
weight: relay.weight,
endpoint_data,
location,
- features,
}
}
@@ -247,21 +245,11 @@ struct Relay {
impl Relay {
fn into_openvpn_mullvad_relay(self, location: location::Location) -> relay_list::Relay {
- into_mullvad_relay(
- self,
- location,
- relay_list::RelayEndpointData::Openvpn,
- relay_list::Features::empty(),
- )
+ into_mullvad_relay(self, location, relay_list::RelayEndpointData::Openvpn)
}
fn into_bridge_mullvad_relay(self, location: location::Location) -> relay_list::Relay {
- into_mullvad_relay(
- self,
- location,
- relay_list::RelayEndpointData::Bridge,
- relay_list::Features::empty(),
- )
+ into_mullvad_relay(self, location, relay_list::RelayEndpointData::Bridge)
}
fn convert_to_lowercase(&mut self) {
@@ -353,25 +341,61 @@ struct WireGuardRelay {
#[serde(default)]
shadowsocks_extra_addr_in: Vec<IpAddr>,
#[serde(default)]
- features: relay_list::Features,
+ features: Features,
}
impl WireGuardRelay {
fn into_mullvad_relay(self, location: location::Location) -> relay_list::Relay {
- // Sanity check that new 'features' key is in sync with the old Relay keys.
- if self.features.daita() {
+ // Sanity check that new 'features' key is in sync with the old, superceded keys.
+ // TODO: Remove `self.daita` (and this check 👇) when `features` key has been completely
+ // rolled out to production.
+ if self.features.daita.is_some() {
debug_assert!(self.daita)
}
- into_mullvad_relay(
- self.relay,
- location,
+
+ let relay = self.relay;
+ let endpoint_data =
relay_list::RelayEndpointData::Wireguard(relay_list::WireguardRelayEndpointData {
public_key: self.public_key,
daita: self.daita,
- shadowsocks_extra_addr_in: self.shadowsocks_extra_addr_in,
- }),
- self.features,
- )
+ shadowsocks_extra_addr_in: HashSet::from_iter(self.shadowsocks_extra_addr_in),
+ quic: self.features.quic.map(relay_list::Quic::from),
+ });
+
+ into_mullvad_relay(relay, location, endpoint_data)
+ }
+}
+
+/// Extra features enabled on some (Wireguard) relay, such as obfuscation daemons or Daita.
+#[derive(Debug, Default, Clone, serde::Deserialize)]
+struct Features {
+ daita: Option<Daita>,
+ quic: Option<Quic>,
+}
+
+/// DAITA doesn't have any configuration options (exposed by the API).
+///
+/// Note, an empty struct is not the same as an empty tuple struct according to serde_json!
+#[derive(Debug, Clone, serde::Deserialize)]
+struct Daita {}
+
+/// Parameters for setting up a QUIC obfuscator (connecting to a masque-proxy running on a relay).
+#[derive(Debug, Clone, serde::Deserialize)]
+struct Quic {
+ /// In-addresses for the QUIC obfuscator.
+ ///
+ /// There may be 0, 1 or 2 in IPs, depending on how many masque-proxy daemons running on the
+ /// relay. Hopefully the API will tell use the correct amount🤞.
+ addr_in: Vec<IpAddr>,
+ /// Authorization token
+ token: String,
+ /// Hostname where masque proxy is hosted
+ domain: String,
+}
+
+impl From<Quic> for relay_list::Quic {
+ fn from(value: Quic) -> Self {
+ Self::new(value.addr_in, value.token, value.domain)
}
}
diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto
index fc453afe9b..4d405bcde2 100644
--- a/mullvad-management-interface/proto/management_interface.proto
+++ b/mullvad-management-interface/proto/management_interface.proto
@@ -6,7 +6,6 @@ import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/duration.proto";
-import "google/protobuf/any.proto";
service ManagementService {
// Control and get tunnel state
@@ -717,20 +716,27 @@ message RelayListCity {
}
message Relay {
- enum RelayType {
- OPENVPN = 0;
- BRIDGE = 1;
- WIREGUARD = 2;
- }
- message Features {
- message Quic {
- string domain = 1;
- string token = 2;
- repeated string addr_in = 3;
+ message RelayData {
+ message OpenVPN {}
+ message Bridge {}
+ message Wireguard {
+ message Quic {
+ string domain = 1;
+ string token = 2;
+ repeated string addr_in = 3;
+ }
+
+ bytes public_key = 1;
+ bool daita = 2;
+ Quic quic = 3;
+ repeated string shadowsocks_extra_addr_in = 4;
}
- bool daita = 1;
- Quic quic = 2;
+ oneof data {
+ Wireguard wireguard = 1;
+ OpenVPN openvpn = 2;
+ Bridge bridge = 3;
+ }
}
string hostname = 1;
@@ -741,17 +747,8 @@ message Relay {
bool owned = 6;
string provider = 7;
fixed64 weight = 8;
- RelayType endpoint_type = 9;
- // XXX: wtf, why is this untyped
- google.protobuf.Any endpoint_data = 10;
- Location location = 11;
- Features features = 12;
-}
-
-message WireguardRelayEndpointData {
- bytes public_key = 1;
- bool daita = 2;
- repeated string shadowsocks_extra_addr_in = 3;
+ RelayData endpoint_data = 9;
+ Location location = 10;
}
message Location {
diff --git a/mullvad-management-interface/src/types/conversions/mod.rs b/mullvad-management-interface/src/types/conversions/mod.rs
index 0654cbb641..3c610b8d1b 100644
--- a/mullvad-management-interface/src/types/conversions/mod.rs
+++ b/mullvad-management-interface/src/types/conversions/mod.rs
@@ -60,22 +60,3 @@ impl From<FromProtobufTypeError> for crate::Status {
}
}
}
-
-/// Converts any message to `google.protobuf.Any`.
-fn to_proto_any<T: prost::Message>(type_name: &str, message: T) -> prost_types::Any {
- prost_types::Any {
- type_url: format!("type.googleapis.com/{type_name}"),
- value: message.encode_to_vec(),
- }
-}
-
-/// Tries to convert a message from `google.protobuf.Any` to `T`.
-fn try_from_proto_any<T: prost::Message + Default>(
- type_name: &str,
- any_value: prost_types::Any,
-) -> Option<T> {
- if any_value.type_url != format!("type.googleapis.com/{type_name}") {
- return None;
- }
- T::decode(any_value.value.as_slice()).ok()
-}
diff --git a/mullvad-management-interface/src/types/conversions/relay_list.rs b/mullvad-management-interface/src/types/conversions/relay_list.rs
index 780900413c..08ec379ba9 100644
--- a/mullvad-management-interface/src/types/conversions/relay_list.rs
+++ b/mullvad-management-interface/src/types/conversions/relay_list.rs
@@ -1,16 +1,11 @@
use std::{
- net::{Ipv4Addr, Ipv6Addr},
+ collections::HashSet,
+ net::{IpAddr, Ipv4Addr, Ipv6Addr},
ops::RangeInclusive,
str::FromStr,
};
-use mullvad_types::relay_list::Features;
-
-use crate::types::{
- FromProtobufTypeError,
- conversions::{bytes_to_pubkey, to_proto_any, try_from_proto_any},
- proto,
-};
+use crate::types::{FromProtobufTypeError, conversions::bytes_to_pubkey, proto};
use super::net::try_transport_protocol_from_i32;
@@ -127,26 +122,29 @@ impl From<mullvad_types::relay_list::Relay> for proto::Relay {
owned: relay.owned,
provider: relay.provider,
weight: relay.weight,
- endpoint_type: match &relay.endpoint_data {
- MullvadEndpointData::Openvpn => proto::relay::RelayType::Openvpn as i32,
- MullvadEndpointData::Bridge => proto::relay::RelayType::Bridge as i32,
- MullvadEndpointData::Wireguard(_) => proto::relay::RelayType::Wireguard as i32,
- },
- endpoint_data: match relay.endpoint_data {
- MullvadEndpointData::Wireguard(data) => Some(to_proto_any(
- "mullvad_daemon.management_interface/WireguardRelayEndpointData",
- proto::WireguardRelayEndpointData {
- public_key: data.public_key.as_bytes().to_vec(),
- // TODO: Deprecate in favor of new `features` key
- daita: data.daita,
- shadowsocks_extra_addr_in: data
- .shadowsocks_extra_addr_in
- .iter()
+ endpoint_data: {
+ use proto::relay::RelayData;
+ use proto::relay::relay_data::{Bridge, Data, OpenVpn, Wireguard, wireguard};
+ let data = match relay.endpoint_data {
+ MullvadEndpointData::Wireguard(data) => {
+ let shadowsocks_extra_addr_in = data
+ .shadowsocks_extra_in_addrs()
.map(|addr| addr.to_string())
- .collect(),
- },
- )),
- _ => None,
+ .collect();
+ let public_key = data.public_key.as_bytes().to_vec();
+ let daita = data.daita;
+ let quic = data.quic.map(wireguard::Quic::from);
+ Data::Wireguard(Wireguard {
+ public_key,
+ daita,
+ shadowsocks_extra_addr_in,
+ quic,
+ })
+ }
+ MullvadEndpointData::Bridge => Data::Bridge(Bridge {}),
+ MullvadEndpointData::Openvpn => Data::Openvpn(OpenVpn {}),
+ };
+ Some(RelayData { data: Some(data) })
},
location: Some(proto::Location {
country: relay.location.country,
@@ -156,53 +154,15 @@ impl From<mullvad_types::relay_list::Relay> for proto::Relay {
latitude: relay.location.latitude,
longitude: relay.location.longitude,
}),
- features: Some(proto::relay::Features::from(relay.features)),
}
}
}
-impl From<mullvad_types::relay_list::Features> for proto::relay::Features {
- fn from(features: mullvad_types::relay_list::Features) -> Self {
- Self {
- daita: features.daita(),
- quic: features
- .quic()
- .cloned()
- .map(proto::relay::features::Quic::from),
- }
- }
-}
-
-impl TryFrom<proto::relay::Features> for mullvad_types::relay_list::Features {
- type Error = FromProtobufTypeError;
-
- fn try_from(value: proto::relay::Features) -> Result<Self, Self::Error> {
- let features = Features::empty();
- let features = if value.daita {
- features.configure_daita()
- } else {
- features
- };
- let features = {
- let quic = value
- .quic
- .map(mullvad_types::relay_list::Quic::try_from)
- .transpose()?;
- if let Some(options) = quic {
- features.configure_quic(options)
- } else {
- features
- }
- };
- Ok(features)
- }
-}
-
-impl From<mullvad_types::relay_list::Quic> for proto::relay::features::Quic {
- fn from(value: mullvad_types::relay_list::Quic) -> Self {
- let domain = value.hostname().to_owned();
- let token = value.auth_token().to_owned();
- let addr_in = value.in_addr().iter().map(|ip| ip.to_string()).collect();
+impl From<mullvad_types::relay_list::Quic> for proto::relay::relay_data::wireguard::Quic {
+ fn from(quic: mullvad_types::relay_list::Quic) -> Self {
+ let domain = quic.hostname().to_owned();
+ let token = quic.auth_token().to_owned();
+ let addr_in = quic.in_addr().map(|ip| ip.to_string()).collect();
Self {
domain,
token,
@@ -211,21 +171,21 @@ impl From<mullvad_types::relay_list::Quic> for proto::relay::features::Quic {
}
}
-impl TryFrom<proto::relay::features::Quic> for mullvad_types::relay_list::Quic {
+impl TryFrom<proto::relay::relay_data::wireguard::Quic> for mullvad_types::relay_list::Quic {
type Error = FromProtobufTypeError;
- fn try_from(value: proto::relay::features::Quic) -> Result<Self, Self::Error> {
+ fn try_from(value: proto::relay::relay_data::wireguard::Quic) -> Result<Self, Self::Error> {
let domain = value.domain;
let token = value.token;
+ fn parse_addr(addr: String) -> Result<IpAddr, FromProtobufTypeError> {
+ addr.parse()
+ .map_err(|_err| FromProtobufTypeError::InvalidArgument("Invalid IP address"))
+ }
let addr_in = value
.addr_in
- .iter()
- .map(|addr| {
- addr.parse().map_err(|_err| {
- FromProtobufTypeError::InvalidArgument("Invalid IP address: {addr}")
- })
- })
- .collect::<Result<_, FromProtobufTypeError>>()?;
+ .into_iter()
+ .map(parse_addr)
+ .collect::<Result<Vec<IpAddr>, FromProtobufTypeError>>()?;
Ok(Self::new(addr_in, token, domain))
}
}
@@ -309,44 +269,43 @@ impl TryFrom<proto::Relay> for mullvad_types::relay_list::Relay {
relay_list::{Relay as MullvadRelay, RelayEndpointData as MullvadEndpointData},
};
- let endpoint_data = match relay.endpoint_type {
- i if i == proto::relay::RelayType::Openvpn as i32 => MullvadEndpointData::Openvpn,
- i if i == proto::relay::RelayType::Bridge as i32 => MullvadEndpointData::Bridge,
- i if i == proto::relay::RelayType::Wireguard as i32 => {
- let data = relay
- .endpoint_data
- .ok_or(FromProtobufTypeError::InvalidArgument(
- "missing endpoint wg data",
- ))?;
- let data: proto::WireguardRelayEndpointData = try_from_proto_any(
- "mullvad_daemon.management_interface/WireguardRelayEndpointData",
- data,
- )
+ let endpoint_data = {
+ let data = relay
+ .endpoint_data
+ .and_then(|endpoint| endpoint.data)
.ok_or(FromProtobufTypeError::InvalidArgument(
- "invalid endpoint wg data",
- ))?;
- MullvadEndpointData::Wireguard(
- mullvad_types::relay_list::WireguardRelayEndpointData {
- public_key: bytes_to_pubkey(&data.public_key)?,
- daita: data.daita,
- shadowsocks_extra_addr_in: data
- .shadowsocks_extra_addr_in
- .iter()
- .map(|addr| {
- addr.parse().map_err(|_err| {
- FromProtobufTypeError::InvalidArgument(
- "invalid relay IPv6 address",
- )
- })
- })
- .collect::<Result<_, FromProtobufTypeError>>()?,
- },
- )
- }
- _ => {
- return Err(FromProtobufTypeError::InvalidArgument(
"invalid relay endpoint type",
- ));
+ ))?;
+ match data {
+ proto::relay::relay_data::Data::Openvpn(_openvpn) => MullvadEndpointData::Openvpn,
+ proto::relay::relay_data::Data::Bridge(_bridge) => MullvadEndpointData::Bridge,
+ proto::relay::relay_data::Data::Wireguard(wireguard) => {
+ fn parse_addr(addr: &str) -> Result<IpAddr, FromProtobufTypeError> {
+ addr.parse().map_err(|_err| {
+ FromProtobufTypeError::InvalidArgument("Invalid IP address")
+ })
+ }
+
+ let public_key = bytes_to_pubkey(&wireguard.public_key)?;
+ let daita = wireguard.daita;
+ let quic = wireguard
+ .quic
+ .map(mullvad_types::relay_list::Quic::try_from)
+ .transpose()?;
+ let shadowsocks_extra_addr_in = wireguard
+ .shadowsocks_extra_addr_in
+ .iter()
+ .map(String::as_ref)
+ .map(parse_addr)
+ .collect::<Result<HashSet<IpAddr>, FromProtobufTypeError>>()?;
+ let data = mullvad_types::relay_list::WireguardRelayEndpointData {
+ public_key,
+ daita,
+ quic,
+ shadowsocks_extra_addr_in,
+ };
+ MullvadEndpointData::Wireguard(data)
+ }
}
};
@@ -359,12 +318,6 @@ impl TryFrom<proto::Relay> for mullvad_types::relay_list::Relay {
})
.transpose()?;
- let features = relay
- .features
- .map(mullvad_types::relay_list::Features::try_from)
- .transpose()?
- .unwrap_or_default();
-
let relay = MullvadRelay {
hostname: relay.hostname,
ipv4_addr_in: relay.ipv4_addr_in.parse().map_err(|_err| {
@@ -391,7 +344,6 @@ impl TryFrom<proto::Relay> for mullvad_types::relay_list::Relay {
})
.ok_or("missing relay location")
.map_err(FromProtobufTypeError::InvalidArgument)?,
- features,
};
Ok(relay)
diff --git a/mullvad-relay-selector/src/relay_selector/helpers.rs b/mullvad-relay-selector/src/relay_selector/helpers.rs
index 7135d41fd0..1a5a11e69c 100644
--- a/mullvad-relay-selector/src/relay_selector/helpers.rs
+++ b/mullvad-relay-selector/src/relay_selector/helpers.rs
@@ -124,7 +124,7 @@ pub fn get_shadowsocks_obfuscator(
let port = settings.port;
let extra_addrs = match &relay.endpoint_data {
mullvad_types::relay_list::RelayEndpointData::Wireguard(wg) => {
- &wg.shadowsocks_extra_addr_in
+ wg.shadowsocks_extra_in_addrs()
}
_ => panic!("expected wireguard relay"),
};
@@ -132,7 +132,7 @@ pub fn get_shadowsocks_obfuscator(
let endpoint = get_shadowsocks_obfuscator_inner(
endpoint.peer.endpoint.ip(),
non_extra_port_ranges,
- extra_addrs,
+ extra_addrs.copied(),
port,
)?;
@@ -143,7 +143,7 @@ pub fn get_shadowsocks_obfuscator(
}
pub fn get_quic_obfuscator(relay: Relay, ip_version: IpVersion) -> Option<SelectedObfuscator> {
- let quic = relay.features.quic()?;
+ let quic = relay.wireguard()?.quic()?;
let config = {
let hostname = quic.hostname().to_string();
let endpoint = match ip_version {
@@ -168,14 +168,13 @@ pub fn get_quic_obfuscator(relay: Relay, ip_version: IpVersion) -> Option<Select
fn get_shadowsocks_obfuscator_inner<R: RangeBounds<u16> + Iterator<Item = u16> + Clone>(
wg_in_addr: IpAddr,
wg_in_addr_port_ranges: &[R],
- extra_in_addrs: &[IpAddr],
+ extra_in_addrs: impl IntoIterator<Item = IpAddr>,
desired_port: Constraint<u16>,
) -> Result<SocketAddr, Error> {
// Filter out addresses for the wrong address family
let extra_in_addrs: Vec<_> = extra_in_addrs
- .iter()
+ .into_iter()
.filter(|addr| addr.is_ipv4() == wg_in_addr.is_ipv4())
- .copied()
.collect();
let in_ip = extra_in_addrs
@@ -244,7 +243,7 @@ mod tests {
SHADOWSOCKS_EXTRA_PORT_RANGES, get_shadowsocks_obfuscator_inner, port_if_in_range,
};
use mullvad_types::constraints::Constraint;
- use std::{net::IpAddr, ops::RangeInclusive};
+ use std::{iter, net::IpAddr, ops::RangeInclusive};
/// Test whether select ports are available when relay has no extra IPs
#[test]
@@ -255,7 +254,7 @@ mod tests {
let wg_in_ip: IpAddr = "1.2.3.4".parse().unwrap();
let selected_addr =
- get_shadowsocks_obfuscator_inner(wg_in_ip, PORT_RANGES, &[], Constraint::Any)
+ get_shadowsocks_obfuscator_inner(wg_in_ip, PORT_RANGES, iter::empty(), Constraint::Any)
.expect("should find valid port without constraint");
assert_eq!(selected_addr.ip(), wg_in_ip);
@@ -267,7 +266,7 @@ mod tests {
let selected_addr = get_shadowsocks_obfuscator_inner(
wg_in_ip,
PORT_RANGES,
- &[],
+ iter::empty(),
Constraint::Only(WITHIN_RANGE_PORT),
)
.expect("should find within-range port");
@@ -281,7 +280,7 @@ mod tests {
let selected_addr = get_shadowsocks_obfuscator_inner(
wg_in_ip,
PORT_RANGES,
- &[],
+ iter::empty(),
Constraint::Only(OUT_OF_RANGE_PORT),
);
assert!(
@@ -297,13 +296,13 @@ mod tests {
const OUT_OF_RANGE_PORT: u16 = 1;
let wg_in_ip: IpAddr = "1.2.3.4".parse().unwrap();
- let extra_in_addrs: &[IpAddr] =
- &["1.3.3.7".parse().unwrap(), "192.0.2.123".parse().unwrap()];
+ let extra_in_addrs: Vec<IpAddr> =
+ vec!["1.3.3.7".parse().unwrap(), "192.0.2.123".parse().unwrap()];
let selected_addr = get_shadowsocks_obfuscator_inner(
wg_in_ip,
PORT_RANGES,
- extra_in_addrs,
+ extra_in_addrs.clone(),
Constraint::Any,
)
.expect("should find valid port without constraint");
@@ -317,7 +316,7 @@ mod tests {
let selected_addr = get_shadowsocks_obfuscator_inner(
wg_in_ip,
PORT_RANGES,
- extra_in_addrs,
+ extra_in_addrs.clone(),
Constraint::Only(OUT_OF_RANGE_PORT),
)
.expect("expected selected address to be returned");
@@ -340,12 +339,12 @@ mod tests {
const OUT_OF_RANGE_PORT: u16 = 1;
let wg_in_ip: IpAddr = "1.2.3.4".parse().unwrap();
- let extra_in_addrs: &[IpAddr] = &["::2".parse().unwrap()];
+ let extra_in_addrs: Vec<IpAddr> = vec!["::2".parse().unwrap()];
let selected_addr = get_shadowsocks_obfuscator_inner(
wg_in_ip,
PORT_RANGES,
- extra_in_addrs,
+ extra_in_addrs.clone(),
Constraint::Any,
)
.expect("should find valid port without constraint");
@@ -359,7 +358,7 @@ mod tests {
let selected_addr = get_shadowsocks_obfuscator_inner(
wg_in_ip,
PORT_RANGES,
- extra_in_addrs,
+ extra_in_addrs.clone(),
Constraint::Only(OUT_OF_RANGE_PORT),
);
assert!(
diff --git a/mullvad-relay-selector/src/relay_selector/matcher.rs b/mullvad-relay-selector/src/relay_selector/matcher.rs
index 8133677b87..52f0cc600a 100644
--- a/mullvad-relay-selector/src/relay_selector/matcher.rs
+++ b/mullvad-relay-selector/src/relay_selector/matcher.rs
@@ -145,7 +145,7 @@ fn filter_on_obfuscation(
)
}
// QUIC is only enabled on some relays
- ObfuscationQuery::Quic => relay.features.quic().is_some(),
+ ObfuscationQuery::Quic => relay.wireguard().is_some_and(|wg| wg.quic().is_some()),
// Other relays are compatible with this query
ObfuscationQuery::Off | ObfuscationQuery::Auto | ObfuscationQuery::Udp2tcp(_) => true,
}
diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs
index 532e78a076..ad91c192d5 100644
--- a/mullvad-relay-selector/tests/relay_selector.rs
+++ b/mullvad-relay-selector/tests/relay_selector.rs
@@ -27,9 +27,9 @@ use mullvad_types::{
RelayConstraints, RelayOverride, RelaySettings, TransportPort,
},
relay_list::{
- BridgeEndpointData, Features, OpenVpnEndpoint, OpenVpnEndpointData, Quic, Relay,
- RelayEndpointData, RelayList, RelayListCity, RelayListCountry, ShadowsocksEndpointData,
- WireguardEndpointData, WireguardRelayEndpointData,
+ BridgeEndpointData, OpenVpnEndpoint, OpenVpnEndpointData, Quic, Relay, RelayEndpointData,
+ RelayList, RelayListCity, RelayListCountry, ShadowsocksEndpointData, WireguardEndpointData,
+ WireguardRelayEndpointData,
},
};
@@ -42,6 +42,10 @@ static DUMMY_LOCATION: LazyLock<Location> = LazyLock::new(|| Location {
longitude: 11.97,
});
+static WIREGUARD_PUBKEY: LazyLock<PublicKey> = LazyLock::new(|| {
+ PublicKey::from_base64("BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=").unwrap()
+});
+
static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList {
etag: None,
countries: vec![RelayListCountry {
@@ -64,25 +68,19 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList {
owned: true,
provider: "provider0".to_string(),
weight: 1,
- endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData {
- public_key: PublicKey::from_base64(
- "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=",
- )
- .unwrap(),
- daita: true,
- shadowsocks_extra_addr_in: vec![],
- }),
+ endpoint_data: RelayEndpointData::Wireguard(
+ WireguardRelayEndpointData::new(WIREGUARD_PUBKEY.clone())
+ .set_daita(true)
+ .set_quic(Quic::new(
+ vec![
+ "185.213.154.68".parse().unwrap(),
+ "2a03:1b20:5:f011::a09f".parse().unwrap(),
+ ],
+ "Bearer test".to_owned(),
+ "se9-wireguard.blockerad.eu".to_owned(),
+ )),
+ ),
location: DUMMY_LOCATION.clone(),
- features: Features::default()
- .configure_daita()
- .configure_quic(Quic::new(
- vec![
- "185.213.154.68".parse().unwrap(),
- "2a03:1b20:5:f011::a09f".parse().unwrap(),
- ],
- "Bearer test".to_owned(),
- "se9-wireguard.blockerad.eu".to_owned(),
- )),
},
Relay {
hostname: "se10-wireguard".to_string(),
@@ -95,16 +93,11 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList {
owned: false,
provider: "provider1".to_string(),
weight: 1,
- endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData {
- public_key: PublicKey::from_base64(
- "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=",
- )
- .unwrap(),
- daita: false,
- shadowsocks_extra_addr_in: vec![],
- }),
+ endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData::new(
+ PublicKey::from_base64("BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=")
+ .unwrap(),
+ )),
location: DUMMY_LOCATION.clone(),
- features: Features::default(),
},
Relay {
hostname: "se11-wireguard".to_string(),
@@ -117,16 +110,14 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList {
owned: false,
provider: "provider2".to_string(),
weight: 1,
- endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData {
- public_key: PublicKey::from_base64(
- "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=",
+ endpoint_data: RelayEndpointData::Wireguard(
+ WireguardRelayEndpointData::new(
+ PublicKey::from_base64("BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=")
+ .unwrap(),
)
- .unwrap(),
- daita: true,
- shadowsocks_extra_addr_in: vec![],
- }),
+ .set_daita(true),
+ ),
location: DUMMY_LOCATION.clone(),
- features: Features::default().configure_daita(),
},
Relay {
hostname: "se-got-001".to_string(),
@@ -141,7 +132,6 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList {
weight: 1,
endpoint_data: RelayEndpointData::Openvpn,
location: DUMMY_LOCATION.clone(),
- features: Features::default(),
},
Relay {
hostname: "se-got-002".to_string(),
@@ -156,7 +146,6 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList {
weight: 1,
endpoint_data: RelayEndpointData::Openvpn,
location: DUMMY_LOCATION.clone(),
- features: Features::default(),
},
Relay {
hostname: "se-got-br-001".to_string(),
@@ -171,7 +160,6 @@ static RELAYS: LazyLock<RelayList> = LazyLock::new(|| RelayList {
weight: 1,
endpoint_data: RelayEndpointData::Bridge,
location: DUMMY_LOCATION.clone(),
- features: Features::default(),
},
SHADOWSOCKS_RELAY.clone(),
],
@@ -250,13 +238,13 @@ static SHADOWSOCKS_RELAY: LazyLock<Relay> = LazyLock::new(|| Relay {
owned: true,
provider: "provider0".to_string(),
weight: 1,
- endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData {
- public_key: PublicKey::from_base64("eaNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=").unwrap(),
- daita: false,
- shadowsocks_extra_addr_in: SHADOWSOCKS_RELAY_EXTRA_ADDRS.to_vec(),
- }),
+ endpoint_data: RelayEndpointData::Wireguard(
+ WireguardRelayEndpointData::new(
+ PublicKey::from_base64("eaNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=").unwrap(),
+ )
+ .add_shadowsocks_extra_in_addrs(SHADOWSOCKS_RELAY_EXTRA_ADDRS.iter().copied()),
+ ),
location: DUMMY_LOCATION.clone(),
- features: Features::default(),
});
const SHADOWSOCKS_RELAY_IPV4: Ipv4Addr = Ipv4Addr::new(123, 123, 123, 1);
const SHADOWSOCKS_RELAY_IPV6: Ipv6Addr = Ipv6Addr::new(0x123, 0, 0, 0, 0, 0, 0, 2);
@@ -586,16 +574,10 @@ fn test_wireguard_entry() {
owned: true,
provider: "provider0".to_string(),
weight: 1,
- endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData {
- public_key: PublicKey::from_base64(
- "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=",
- )
- .unwrap(),
- daita: false,
- shadowsocks_extra_addr_in: vec![],
- }),
+ endpoint_data: RelayEndpointData::Wireguard(
+ WireguardRelayEndpointData::new(WIREGUARD_PUBKEY.clone()),
+ ),
location: DUMMY_LOCATION.clone(),
- features: Features::default(),
},
Relay {
hostname: "se10-wireguard".to_string(),
@@ -608,16 +590,10 @@ fn test_wireguard_entry() {
owned: false,
provider: "provider1".to_string(),
weight: 1,
- endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData {
- public_key: PublicKey::from_base64(
- "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=",
- )
- .unwrap(),
- daita: false,
- shadowsocks_extra_addr_in: vec![],
- }),
+ endpoint_data: RelayEndpointData::Wireguard(
+ WireguardRelayEndpointData::new(WIREGUARD_PUBKEY.clone()),
+ ),
location: DUMMY_LOCATION.clone(),
- features: Features::default(),
},
],
}],
@@ -1280,16 +1256,10 @@ fn test_include_in_country() {
owned: true,
provider: "31173".to_string(),
weight: 1,
- endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData {
- public_key: PublicKey::from_base64(
- "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=",
- )
- .unwrap(),
- shadowsocks_extra_addr_in: vec![],
- daita: false,
- }),
+ endpoint_data: RelayEndpointData::Wireguard(
+ WireguardRelayEndpointData::new(WIREGUARD_PUBKEY.clone()),
+ ),
location: DUMMY_LOCATION.clone(),
- features: Features::default(),
},
Relay {
hostname: "se10-wireguard".to_string(),
@@ -1302,16 +1272,10 @@ fn test_include_in_country() {
owned: false,
provider: "31173".to_string(),
weight: 1,
- endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData {
- public_key: PublicKey::from_base64(
- "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=",
- )
- .unwrap(),
- shadowsocks_extra_addr_in: vec![],
- daita: false,
- }),
+ endpoint_data: RelayEndpointData::Wireguard(
+ WireguardRelayEndpointData::new(WIREGUARD_PUBKEY.clone()),
+ ),
location: DUMMY_LOCATION.clone(),
- features: Features::default(),
},
],
}],
diff --git a/mullvad-types/src/relay_list.rs b/mullvad-types/src/relay_list.rs
index aceff060b0..c6f1693c14 100644
--- a/mullvad-types/src/relay_list.rs
+++ b/mullvad-types/src/relay_list.rs
@@ -1,6 +1,7 @@
use crate::location::{CityCode, CountryCode, Location};
use serde::{Deserialize, Serialize};
use std::{
+ collections::HashSet,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
ops::RangeInclusive,
};
@@ -88,69 +89,28 @@ pub struct Relay {
pub weight: u64,
pub endpoint_data: RelayEndpointData,
pub location: Location,
- #[serde(default)]
- pub features: Features,
-}
-
-/// Extra features enabled on some (Wireguard) relay, such as obfuscation daemons or Daita.
-#[derive(Debug, Clone, Deserialize, Serialize)]
-pub struct Features {
- daita: Option<Daita>,
- quic: Option<Quic>,
}
-impl Features {
- /// Equivalent to a relay without any additional features.
- pub fn empty() -> Features {
- Features {
- daita: None,
- quic: None,
+impl Relay {
+ /// If self is a Wireguard relay, we sometimes want to peek on its extra data.
+ pub fn wireguard(&self) -> Option<&WireguardRelayEndpointData> {
+ match &self.endpoint_data {
+ RelayEndpointData::Wireguard(wireguard_relay_endpoint_data) => {
+ Some(wireguard_relay_endpoint_data)
+ }
+ RelayEndpointData::Openvpn | RelayEndpointData::Bridge => None,
}
}
-
- /// Whether Daita is enabled
- pub fn daita(&self) -> bool {
- self.daita.is_some()
- }
-
- /// Whether Quic is enabled and its config
- pub fn quic(&self) -> Option<&Quic> {
- self.quic.as_ref()
- }
-
- /// Enable Daita for this relay
- pub fn configure_daita(self) -> Self {
- let daita = Some(Daita {});
- Self { daita, ..self }
- }
-
- /// Configure QUIC for this relay
- pub fn configure_quic(self, options: Quic) -> Self {
- let quic = Some(options);
- Self { quic, ..self }
- }
-}
-
-impl Default for Features {
- fn default() -> Self {
- Features::empty()
- }
}
-/// DAITA doesn't have any configuration options (exposed by the API).
-///
-/// Note, an empty struct is not the same as an empty tuple struct according to serde_json!
-#[derive(Debug, Clone, Deserialize, Serialize)]
-pub struct Daita {}
-
/// Parameters for setting up a QUIC obfuscator (connecting to a masque-proxy running on a relay).
-#[derive(Debug, Clone, Deserialize, Serialize)]
+#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub struct Quic {
/// In-addresses for the QUIC obfuscator.
///
/// There may be 0, 1 or 2 in IPs, depending on how many masque-proxy daemons running on the
/// relay. Hopefully the API will tell use the correct amount🤞.
- addr_in: Vec<IpAddr>,
+ addr_in: HashSet<IpAddr>,
/// Authorization token
token: String,
/// Hostname where masque proxy is hosted
@@ -158,7 +118,8 @@ pub struct Quic {
}
impl Quic {
- pub fn new(addr_in: Vec<IpAddr>, token: String, domain: String) -> Self {
+ pub fn new(addr_in: impl IntoIterator<Item = IpAddr>, token: String, domain: String) -> Self {
+ let addr_in = HashSet::from_iter(addr_in);
Self {
addr_in,
token,
@@ -202,8 +163,8 @@ impl Quic {
&self.token
}
- pub fn in_addr(&self) -> &[IpAddr] {
- &self.addr_in
+ pub fn in_addr(&self) -> impl Iterator<Item = IpAddr> {
+ self.addr_in.iter().copied()
}
}
@@ -234,7 +195,7 @@ impl PartialEq for Relay {
/// # Example
///
/// ```rust
- /// # use mullvad_types::{relay_list::{Relay, Features}, relay_list::{RelayEndpointData, WireguardRelayEndpointData}};
+ /// # use mullvad_types::{relay_list::Relay, relay_list::{RelayEndpointData, WireguardRelayEndpointData}};
/// # use talpid_types::net::wireguard::PublicKey;
///
/// let relay = Relay {
@@ -254,7 +215,8 @@ impl PartialEq for Relay {
/// # )
/// # .unwrap(),
/// # daita: false,
- /// # shadowsocks_extra_addr_in: vec![],
+ /// # shadowsocks_extra_addr_in: Default::default(),
+ /// # quic: None,
/// # }),
/// # location: mullvad_types::location::Location {
/// # country: "Sweden".to_string(),
@@ -264,7 +226,6 @@ impl PartialEq for Relay {
/// # latitude: 57.71,
/// # longitude: 11.97,
/// # },
- /// # features: Features::default(),
/// };
///
/// let mut different_relay = relay.clone();
@@ -342,17 +303,62 @@ impl Default for WireguardEndpointData {
}
/// Contains data about specific WireGuard endpoints, i.e. their public keys.
-#[derive(Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Debug)]
+#[derive(Clone, Eq, PartialEq, Deserialize, Serialize, Debug)]
pub struct WireguardRelayEndpointData {
/// Public key used by the relay peer
pub public_key: wireguard::PublicKey,
- /// Whether the server supports DAITA
- /// FIXME: This has been superceded by [Features] + [Daita].
+ /// Whether the relay supports DAITA
#[serde(default)]
pub daita: bool,
+ /// Parameters for connecting to the masque-proxy running on the relay.
+ #[serde(default)]
+ pub quic: Option<Quic>,
/// Optional IP addresses used by Shadowsocks
#[serde(default)]
- pub shadowsocks_extra_addr_in: Vec<IpAddr>,
+ pub shadowsocks_extra_addr_in: HashSet<IpAddr>,
+}
+
+impl WireguardRelayEndpointData {
+ pub fn new(public_key: wireguard::PublicKey) -> Self {
+ Self {
+ public_key,
+ daita: Default::default(),
+ quic: Default::default(),
+ shadowsocks_extra_addr_in: Default::default(),
+ }
+ }
+
+ pub fn set_daita(self, enabled: bool) -> Self {
+ Self {
+ daita: enabled,
+ ..self
+ }
+ }
+
+ pub fn set_quic(self, quic: Quic) -> Self {
+ Self {
+ quic: Some(quic),
+ ..self
+ }
+ }
+
+ /// Add `in_addrs` to the existing shadowsocks extra in addressess.
+ pub fn add_shadowsocks_extra_in_addrs(self, in_addrs: impl Iterator<Item = IpAddr>) -> Self {
+ let in_addrs = self.shadowsocks_extra_in_addrs().copied().chain(in_addrs);
+ Self {
+ shadowsocks_extra_addr_in: HashSet::from_iter(in_addrs),
+ ..self
+ }
+ }
+
+ pub fn shadowsocks_extra_in_addrs(&self) -> impl Iterator<Item = &IpAddr> {
+ self.shadowsocks_extra_addr_in.iter()
+ }
+
+ // Is this really needed if `self.quic` is pub?
+ pub fn quic(&self) -> Option<&Quic> {
+ self.quic.as_ref()
+ }
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]