Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions e2e/ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,10 @@ test.describe('settings @smoke', () => {
.first()
.first()
await networksMenu().click()
await page().getByRole('button', { name: 'Edit network' }).first().click()
await page()
.getByRole('button', { name: /^Edit / })
.first()
.click()
await page().getByRole('textbox', { name: 'Name' }).fill('Office fibre')
await page().getByRole('button', { name: 'Violet' }).click()
await page().getByRole('button', { name: 'Done' }).click()
Expand All @@ -326,7 +329,10 @@ test.describe('settings @smoke', () => {
await page().keyboard.press('Escape')
await networksMenu().click()
await expect(page().getByText('Office fibre')).toBeVisible()
await page().getByRole('button', { name: 'Edit network' }).first().click()
await page()
.getByRole('button', { name: /^Edit / })
.first()
.click()
await expect(page().getByRole('button', { name: 'Violet' })).toHaveAttribute(
'aria-pressed',
'true'
Expand Down
33 changes: 30 additions & 3 deletions src/main/network/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { execFile } from 'node:child_process'
import { existsSync } from 'node:fs'
import { readdir, readlink } from 'node:fs/promises'
import { readdir, readFile, readlink, realpath } from 'node:fs/promises'
import { networkInterfaces } from 'node:os'
import { promisify } from 'node:util'
import type { NetworkAddress, NetworkInterfaceInfo, NetworkInterfaceKind } from '../../shared/types'
Expand All @@ -18,6 +18,21 @@ interface WindowsAdapter {
interface LinuxAdapter {
displayName: string
kind: NetworkInterfaceKind
/** The USB serial of the device behind a USB network adapter, such as a tethered phone. */
usbSerial?: string
}

/** A phone shared over USB gets a random MAC each time it's plugged in, and so a new enx… name
* that matches nothing saved for it. The serial its USB device reports doesn't change: the network
* function is …/usb1/1-2/1-2:1.0, and its parent, 1-2, is the phone. */
async function readUsbSerial(sysPath: string): Promise<string | undefined> {
try {
const usbDevice = await realpath(`${sysPath}/device/..`)
if (!usbDevice.includes('/usb')) return undefined
return (await readFile(`${usbDevice}/serial`, 'utf8')).trim() || undefined
} catch {
return undefined
}
}

/**
Expand Down Expand Up @@ -187,10 +202,21 @@ async function getLinuxAdapters(): Promise<Map<string, LinuxAdapter>> {
? 'USB Network'
: device)

adapters.set(device, { displayName, kind })
const usbSerial = kind === 'usb' ? await readUsbSerial(sysPath) : undefined
adapters.set(device, { displayName, kind, usbSerial })
})
)

// Ids must be unique: a serial two devices share (cheap phones that all report
// 0123456789ABCDEF, or one phone with two network functions) names neither of them.
const serials = [...adapters.values()].flatMap((adapter) =>
adapter.usbSerial ? [adapter.usbSerial] : []
)
const shared = new Set(serials.filter((serial, i) => serials.indexOf(serial) !== i))
for (const adapter of adapters.values()) {
if (adapter.usbSerial && shared.has(adapter.usbSerial)) delete adapter.usbSerial
}

return adapters
}

Expand Down Expand Up @@ -281,7 +307,8 @@ export async function listActiveInterfaces(): Promise<NetworkInterfaceInfo[]> {
const displayName = linuxAdapter?.displayName ?? hardwareName ?? device

result.push({
id: device,
// A tethered phone keeps its saved name and color across replugs (see readUsbSerial).
id: linuxAdapter?.usbSerial ? `usb:${linuxAdapter.usbSerial}` : device,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the id for every Linux USB adapter with a serial, including USB-Ethernet dongles with a stable enx… name. Settings (name, color, limits, the #32 proxy) are keyed by this id, so they'd be lost on upgrade. Please move an entry saved under device to the new usb: id when there's none under the new one.

device,
displayName,
addresses: usable,
Expand Down
3 changes: 2 additions & 1 deletion src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export interface NetworkAddress {
export type ThemeSource = 'light' | 'dark'

export interface NetworkInterfaceInfo {
/** Stable identifier for this interface (currently the OS device name, e.g. "en0"). */
/** Stable identifier for this interface: the OS device name (e.g. "en0"), or "usb:<serial>" for
* a phone tethered over USB on Linux, whose device name changes each time it is plugged in. */
id: string
device: string
displayName: string
Expand Down
Loading