mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(clients): use hardware id for device verification (#6857)
We want to associate additional device information for the device verification, these are all parameters that tries to uniquely identify the hardware. For that reason we read system information and send it as part of the query params to the portal, that way the portal can store this when device is verified and match against that later on. These are the parameters according to each platform: |Platform|Query Field|Field Meaning| |-----|----|-----| |MacOS|`device_serial`|Hardware's Serial| |MacOS| `device_uuid`|Hardware's UUID| |iOS|`identifier_for_vendor`| Identifier for vendor, resets only on uninstall/install| |Android|`firebase_installation_id`| Firebase installation ID, resets only on uninstall/install| |Windows|`device_serial`|Motherboard's Serial| |Linux|`device_serial`|Motherboard's Serial| Fixes #6837
This commit is contained in:
@@ -180,6 +180,8 @@ dependencies {
|
||||
// Hilt
|
||||
implementation("com.google.dagger:hilt-android:2.52")
|
||||
implementation("androidx.browser:browser:1.8.0")
|
||||
implementation("com.google.firebase:firebase-installations-ktx:18.0.0")
|
||||
implementation("com.google.android.gms:play-services-tasks:18.2.0")
|
||||
kapt("androidx.hilt:hilt-compiler:1.2.0")
|
||||
kapt("com.google.dagger:hilt-android-compiler:2.52")
|
||||
// Instrumented Tests
|
||||
@@ -217,7 +219,7 @@ dependencies {
|
||||
androidTestImplementation("androidx.fragment:fragment-testing:1.8.2")
|
||||
|
||||
// Import the BoM for the Firebase platform
|
||||
implementation(platform("com.google.firebase:firebase-bom:33.2.0"))
|
||||
implementation(platform("com.google.firebase:firebase-bom:33.3.0"))
|
||||
|
||||
// Add the dependencies for the Crashlytics and Analytics libraries
|
||||
// When using the BoM, you don't specify versions in Firebase library dependencies
|
||||
|
||||
@@ -11,6 +11,7 @@ object ConnlibSession {
|
||||
logDir: String,
|
||||
logFilter: String,
|
||||
callback: Any,
|
||||
deviceInfo: String,
|
||||
): Long
|
||||
|
||||
external fun disconnect(connlibSession: Long): Boolean
|
||||
|
||||
@@ -16,8 +16,13 @@ import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.google.android.gms.tasks.Tasks
|
||||
import com.google.firebase.installations.FirebaseInstallations
|
||||
import com.google.gson.FieldNamingPolicy
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -30,10 +35,14 @@ import dev.firezone.android.tunnel.model.Resource
|
||||
import dev.firezone.android.tunnel.model.isInternetResource
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import javax.inject.Inject
|
||||
|
||||
data class DeviceInfo(
|
||||
var firebaseInstallationId: String? = null,
|
||||
)
|
||||
|
||||
@AndroidEntryPoint
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class TunnelService : VpnService() {
|
||||
@@ -317,20 +326,40 @@ class TunnelService : VpnService() {
|
||||
tunnelState = State.CONNECTING
|
||||
updateStatusNotification(TunnelStatusNotification.Connecting)
|
||||
|
||||
connlibSessionPtr =
|
||||
ConnlibSession.connect(
|
||||
apiUrl = config.apiUrl,
|
||||
token = token,
|
||||
deviceId = deviceId(),
|
||||
deviceName = getDeviceName(),
|
||||
osVersion = Build.VERSION.RELEASE,
|
||||
logDir = getLogDir(),
|
||||
logFilter = config.logFilter,
|
||||
callback = callback,
|
||||
)
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
startNetworkMonitoring()
|
||||
startDisconnectMonitoring()
|
||||
executor.execute {
|
||||
val deviceInfo = DeviceInfo()
|
||||
|
||||
runCatching {
|
||||
Tasks.await(FirebaseInstallations.getInstance().id)
|
||||
}.onSuccess { firebaseInstallationId ->
|
||||
deviceInfo.firebaseInstallationId = firebaseInstallationId
|
||||
}.onFailure { exception ->
|
||||
Log.d(TAG, "Failed to obtain firebase installation id: $exception")
|
||||
}
|
||||
|
||||
val gson: Gson =
|
||||
GsonBuilder()
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
|
||||
.create()
|
||||
|
||||
connlibSessionPtr =
|
||||
ConnlibSession.connect(
|
||||
apiUrl = config.apiUrl,
|
||||
token = token,
|
||||
deviceId = deviceId(),
|
||||
deviceName = getDeviceName(),
|
||||
osVersion = Build.VERSION.RELEASE,
|
||||
logDir = getLogDir(),
|
||||
logFilter = config.logFilter,
|
||||
callback = callback,
|
||||
deviceInfo = gson.toJson(deviceInfo),
|
||||
)
|
||||
|
||||
startNetworkMonitoring()
|
||||
startDisconnectMonitoring()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,10 +433,11 @@ class TunnelService : VpnService() {
|
||||
// Get the deviceId from the preferenceRepository, or save a new UUIDv4 and return that if it doesn't exist
|
||||
val deviceId =
|
||||
repo.getDeviceIdSync() ?: run {
|
||||
val newDeviceId = UUID.randomUUID().toString()
|
||||
val newDeviceId = java.util.UUID.randomUUID().toString()
|
||||
repo.saveDeviceIdSync(newDeviceId)
|
||||
newDeviceId
|
||||
}
|
||||
|
||||
return deviceId
|
||||
}
|
||||
|
||||
@@ -441,6 +471,7 @@ class TunnelService : VpnService() {
|
||||
|
||||
private const val SESSION_NAME: String = "Firezone Connection"
|
||||
private const val MTU: Int = 1280
|
||||
private const val TAG: String = "TunnelService"
|
||||
|
||||
private val MANAGED_CONFIGURATIONS = arrayOf("token", "allowedApplications", "disallowedApplications", "deviceName")
|
||||
|
||||
|
||||
45
rust/Cargo.lock
generated
45
rust/Cargo.lock
generated
@@ -2373,6 +2373,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_variant",
|
||||
"smbios-lib",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
@@ -2878,6 +2879,15 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
@@ -3840,6 +3850,16 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-kit-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"mach2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ip-packet"
|
||||
version = "0.1.0"
|
||||
@@ -4299,6 +4319,15 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
@@ -6841,6 +6870,22 @@ version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "smbios-lib"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c18320ad3d997a100cb948fc020111936c530eddfde947f467083730e39e72"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.0",
|
||||
"core-foundation-sys",
|
||||
"getopts",
|
||||
"io-kit-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smithay-client-toolkit"
|
||||
version = "0.19.2"
|
||||
|
||||
@@ -332,6 +332,7 @@ fn connect(
|
||||
log_dir: JString,
|
||||
log_filter: JString,
|
||||
callback_handler: GlobalRef,
|
||||
device_info: JString,
|
||||
) -> Result<SessionWrapper, ConnectError> {
|
||||
let api_url = string_from_jstring!(env, api_url);
|
||||
let secret = SecretString::from(string_from_jstring!(env, token));
|
||||
@@ -340,6 +341,9 @@ fn connect(
|
||||
let os_version = string_from_jstring!(env, os_version);
|
||||
let log_dir = string_from_jstring!(env, log_dir);
|
||||
let log_filter = string_from_jstring!(env, log_filter);
|
||||
let device_info = string_from_jstring!(env, device_info);
|
||||
|
||||
let device_info = serde_json::from_str(&device_info).unwrap();
|
||||
|
||||
let handle = init_logging(&PathBuf::from(log_dir), log_filter);
|
||||
install_rustls_crypto_provider();
|
||||
@@ -357,6 +361,7 @@ fn connect(
|
||||
device_id,
|
||||
Some(device_name),
|
||||
public_key.to_bytes(),
|
||||
device_info,
|
||||
)?;
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
@@ -407,6 +412,7 @@ pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_ConnlibSession_co
|
||||
log_dir: JString,
|
||||
log_filter: JString,
|
||||
callback_handler: JObject,
|
||||
device_info: JString,
|
||||
) -> *const SessionWrapper {
|
||||
let Ok(callback_handler) = env.new_global_ref(callback_handler) else {
|
||||
return std::ptr::null();
|
||||
@@ -423,6 +429,7 @@ pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_ConnlibSession_co
|
||||
log_dir,
|
||||
log_filter,
|
||||
callback_handler,
|
||||
device_info,
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ mod ffi {
|
||||
log_dir: String,
|
||||
log_filter: String,
|
||||
callback_handler: CallbackHandler,
|
||||
device_info: String,
|
||||
) -> Result<WrappedSession, String>;
|
||||
|
||||
fn reset(&mut self);
|
||||
@@ -187,11 +188,13 @@ impl WrappedSession {
|
||||
log_dir: String,
|
||||
log_filter: String,
|
||||
callback_handler: ffi::CallbackHandler,
|
||||
device_info: String,
|
||||
) -> Result<Self> {
|
||||
let logger = init_logging(log_dir.into(), log_filter)?;
|
||||
install_rustls_crypto_provider();
|
||||
|
||||
let secret = SecretString::from(token);
|
||||
let device_info = serde_json::from_str(&device_info).unwrap();
|
||||
|
||||
let (private_key, public_key) = keypair();
|
||||
let url = LoginUrl::client(
|
||||
@@ -200,6 +203,7 @@ impl WrappedSession {
|
||||
device_id,
|
||||
device_name_override,
|
||||
public_key.to_bytes(),
|
||||
device_info,
|
||||
)?;
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
|
||||
@@ -26,6 +26,7 @@ secrecy = { workspace = true }
|
||||
serde = { version = "1.0.210", features = ["derive"] }
|
||||
serde_json = "1.0.125"
|
||||
serde_variant = "0.1.3"
|
||||
smbios-lib = "0.9.2"
|
||||
thiserror = { version = "1.0", default-features = false }
|
||||
# This actually relies on many other features in Tokio, so this will probably
|
||||
# fail to build outside the workspace. <https://github.com/firezone/firezone/pull/4328#discussion_r1540342142>
|
||||
|
||||
@@ -21,6 +21,28 @@ pub(crate) fn path() -> Result<PathBuf> {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn device_serial() -> Option<String> {
|
||||
const DEFAULT_SERIAL: &str = "123456789";
|
||||
let data = smbioslib::table_load_from_device().ok()?;
|
||||
|
||||
let serial = data.find_map(|sys_info: smbioslib::SMBiosSystemInformation| {
|
||||
sys_info.serial_number().to_utf8_lossy()
|
||||
})?;
|
||||
|
||||
if serial == DEFAULT_SERIAL {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(serial)
|
||||
}
|
||||
|
||||
pub fn device_info() -> phoenix_channel::DeviceInfo {
|
||||
phoenix_channel::DeviceInfo {
|
||||
device_serial: device_serial(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the device ID, generating it and saving it to disk if needed.
|
||||
///
|
||||
/// Per <https://github.com/firezone/firezone/issues/2697> and <https://github.com/firezone/firezone/issues/2711>,
|
||||
|
||||
@@ -533,6 +533,7 @@ impl<'a> Handler<'a> {
|
||||
device_id.id,
|
||||
None,
|
||||
public_key.to_bytes(),
|
||||
device_id::device_info(),
|
||||
)
|
||||
.map_err(|e| Error::LoginUrl(e.to_string()))?;
|
||||
|
||||
|
||||
@@ -173,6 +173,7 @@ fn main() -> Result<()> {
|
||||
firezone_id,
|
||||
cli.firezone_name,
|
||||
public_key.to_bytes(),
|
||||
device_id::device_info(),
|
||||
)?;
|
||||
|
||||
if cli.check {
|
||||
|
||||
@@ -27,7 +27,7 @@ use tokio_tungstenite::{
|
||||
};
|
||||
use url::{Host, Url};
|
||||
|
||||
pub use login_url::{LoginUrl, LoginUrlError};
|
||||
pub use login_url::{DeviceInfo, LoginUrl, LoginUrlError};
|
||||
|
||||
pub struct PhoenixChannel<TInitReq, TInboundMsg, TOutboundRes> {
|
||||
state: State,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use secrecy::{CloneableSecret, ExposeSecret as _, SecretString, Zeroize};
|
||||
use serde::Deserialize;
|
||||
use sha2::Digest as _;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use url::Url;
|
||||
@@ -17,6 +18,14 @@ use uuid::Uuid;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const HOST_NAME_MAX: usize = 256;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
pub struct DeviceInfo {
|
||||
pub device_uuid: Option<String>,
|
||||
pub device_serial: Option<String>,
|
||||
pub identifier_for_vendor: Option<String>,
|
||||
pub firebase_installation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LoginUrl {
|
||||
url: Url,
|
||||
@@ -44,8 +53,8 @@ impl LoginUrl {
|
||||
device_id: String,
|
||||
device_name: Option<String>,
|
||||
public_key: [u8; 32],
|
||||
device_info: DeviceInfo,
|
||||
) -> Result<Self, LoginUrlError<E>> {
|
||||
let external_id = hex::encode(sha2::Sha256::digest(device_id));
|
||||
let device_name = device_name
|
||||
.or(get_host_name())
|
||||
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||
@@ -55,11 +64,12 @@ impl LoginUrl {
|
||||
firezone_token,
|
||||
"client",
|
||||
Some(public_key),
|
||||
Some(external_id),
|
||||
Some(device_id),
|
||||
Some(device_name),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
device_info,
|
||||
)?;
|
||||
|
||||
Ok(LoginUrl {
|
||||
@@ -90,6 +100,7 @@ impl LoginUrl {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
Ok(LoginUrl {
|
||||
@@ -116,6 +127,7 @@ impl LoginUrl {
|
||||
Some(listen_port),
|
||||
ipv4_address,
|
||||
ipv6_address,
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
Ok(LoginUrl {
|
||||
@@ -183,6 +195,7 @@ fn get_websocket_path<E>(
|
||||
port: Option<u16>,
|
||||
ipv4_address: Option<Ipv4Addr>,
|
||||
ipv6_address: Option<Ipv6Addr>,
|
||||
device_info: DeviceInfo,
|
||||
) -> Result<Url, LoginUrlError<E>> {
|
||||
set_ws_scheme(&mut api_url)?;
|
||||
|
||||
@@ -219,6 +232,18 @@ fn get_websocket_path<E>(
|
||||
if let Some(port) = port {
|
||||
query_pairs.append_pair("port", &port.to_string());
|
||||
}
|
||||
if let Some(device_serial) = device_info.device_serial {
|
||||
query_pairs.append_pair("device_serial", &device_serial);
|
||||
}
|
||||
if let Some(device_uuid) = device_info.device_uuid {
|
||||
query_pairs.append_pair("device_uuid", &device_uuid);
|
||||
}
|
||||
if let Some(identifier_for_vendor) = device_info.identifier_for_vendor {
|
||||
query_pairs.append_pair("identifier_for_vendor", &identifier_for_vendor);
|
||||
}
|
||||
if let Some(firebase_installation_id) = device_info.firebase_installation_id {
|
||||
query_pairs.append_pair("firebase_installation_id", &firebase_installation_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(api_url)
|
||||
|
||||
@@ -46,24 +46,70 @@ public class DeviceMetadata {
|
||||
// if that doesn't exist. The Firezone ID is a UUIDv4 that is used to dedup this device
|
||||
// for upsert and identification in the admin portal.
|
||||
public static func getOrCreateFirezoneId() -> String {
|
||||
let fileURL = SharedAccess.baseFolderURL.appendingPathComponent("firezone-id")
|
||||
|
||||
do {
|
||||
return try String(contentsOf: fileURL, encoding: .utf8)
|
||||
} catch {
|
||||
// Handle the error if the file doesn't exist or isn't readable
|
||||
// Recreate the file, save a new UUIDv4, and return it
|
||||
let newUUIDString = UUID().uuidString
|
||||
let fileURL = SharedAccess.baseFolderURL.appendingPathComponent("firezone-id")
|
||||
|
||||
do {
|
||||
try newUUIDString.write(to: fileURL, atomically: true, encoding: .utf8)
|
||||
return try String(contentsOf: fileURL, encoding: .utf8)
|
||||
} catch {
|
||||
Log.app.error(
|
||||
"\(#function): Could not save firezone-id file \(fileURL.path)! Error: \(error)"
|
||||
)
|
||||
}
|
||||
// Handle the error if the file doesn't exist or isn't readable
|
||||
// Recreate the file, save a new UUIDv4, and return it
|
||||
let newUUIDString = UUID().uuidString
|
||||
|
||||
return newUUIDString
|
||||
do {
|
||||
try newUUIDString.write(to: fileURL, atomically: true, encoding: .utf8)
|
||||
} catch {
|
||||
Log.app.error(
|
||||
"\(#function): Could not save firezone-id file \(fileURL.path)! Error: \(error)"
|
||||
)
|
||||
}
|
||||
|
||||
return newUUIDString
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
public static func deviceInfo() -> DeviceInfo {
|
||||
return DeviceInfo(identifierForVendor: UIDevice.current.identifierForVendor!.uuidString)
|
||||
}
|
||||
#else
|
||||
public static func deviceInfo() -> DeviceInfo {
|
||||
return DeviceInfo(deviceUuid: getDeviceUuid()!, deviceSerial: getDeviceSerial()!)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
public struct DeviceInfo: Encodable {
|
||||
let identifierForVendor: String
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
import IOKit
|
||||
|
||||
public struct DeviceInfo: Encodable {
|
||||
let deviceUuid: String
|
||||
let deviceSerial: String
|
||||
}
|
||||
|
||||
func getDeviceUuid() -> String? {
|
||||
return getDeviceInfo(key: kIOPlatformUUIDKey as CFString)
|
||||
}
|
||||
|
||||
func getDeviceSerial() -> String? {
|
||||
return getDeviceInfo(key: kIOPlatformSerialNumberKey as CFString)
|
||||
}
|
||||
|
||||
func getDeviceInfo(key: CFString) -> String? {
|
||||
let matchingDict = IOServiceMatching("IOPlatformExpertDevice")
|
||||
|
||||
let platformExpert = IOServiceGetMatchingService(kIOMainPortDefault, matchingDict)
|
||||
defer { IOObjectRelease(platformExpert) }
|
||||
|
||||
if let serial = IORegistryEntryCreateCFProperty(platformExpert, key, kCFAllocatorDefault, 0)?.takeUnretainedValue() as? String {
|
||||
return serial
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -129,6 +129,8 @@ class Adapter {
|
||||
|
||||
Log.tunnel.log("Adapter.start: Starting connlib")
|
||||
do {
|
||||
let jsonEncoder = JSONEncoder()
|
||||
jsonEncoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
// Grab a session pointer
|
||||
let session =
|
||||
try WrappedSession.connect(
|
||||
@@ -139,7 +141,8 @@ class Adapter {
|
||||
DeviceMetadata.getOSVersion(),
|
||||
connlibLogFolderPath,
|
||||
logFilter,
|
||||
callbackHandler
|
||||
callbackHandler,
|
||||
String(data: jsonEncoder.encode(DeviceMetadata.deviceInfo()), encoding: .utf8)!
|
||||
)
|
||||
|
||||
// Start listening for network change events. The first few will be our
|
||||
|
||||
@@ -20,6 +20,9 @@ export default function Android() {
|
||||
Fixes connectivity issues on idle connections by entering an
|
||||
always-on, low-power mode instead of closing them.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="6857">
|
||||
Sends the Firebase Installation ID for device verification.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.3.4" date={new Date("2024-09-26")}>
|
||||
<ChangeItem pull="6809">
|
||||
|
||||
@@ -20,6 +20,12 @@ export default function Apple() {
|
||||
Fixes connectivity issues on idle connections by entering an
|
||||
always-on, low-power mode instead of closing them.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="6857">
|
||||
MacOS: sends hardware's UUID for device verification.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="6857">
|
||||
iOS: sends Id for vendor for device verification.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.3.5" date={new Date("2024-09-26")}>
|
||||
<ChangeItem pull="6809">
|
||||
|
||||
@@ -29,6 +29,9 @@ export default function GUI({ title }: { title: string }) {
|
||||
<ChangeItem enable={title === "Windows"} pull="6874">
|
||||
Fixes a delay when closing the GUI.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="6857">
|
||||
Tries to send motherboard's hardware ID for device verification.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.3.6" date={new Date("2024-09-25")}>
|
||||
<ChangeItem pull="6809">
|
||||
|
||||
@@ -23,6 +23,9 @@ export default function Headless() {
|
||||
<ChangeItem pull="6782">
|
||||
Adds always-on error reporting using sentry.io.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="6857">
|
||||
Sends the motherboard's hardware ID for device verification.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.3.3" date={new Date("2024-09-25")}>
|
||||
<ChangeItem pull="6809">
|
||||
|
||||
Reference in New Issue
Block a user