Skip to content
Merged
Changes from all 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
16 changes: 14 additions & 2 deletions frontend/src/pages/AccountsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,19 @@ function formatBytes(bytes: number): string {
return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`
}

// Mirrors the backend's confFilename (subscription.go): WireGuard clients derive
// the tunnel name from the imported file's name, and Android/wg-quick tunnel names
// must be 1-15 chars of [a-zA-Z0-9_=+.-] - a raw account label (Unicode, spaces,
// or just too long) makes the app reject the file with "Invalid name".
function confFilename(label: string): string {
let stem = label.toLowerCase().replace(/[^a-z0-9._-]+/g, '-')
if (stem.length > 15) stem = stem.slice(0, 15)
// Trim AFTER truncating so a cut landing on a separator doesn't leave a
// trailing "-"/"." right before the extension.
stem = stem.replace(/^[-.]+|[-.]+$/g, '')
return `${stem === '' ? 'wgpanel' : stem}.conf`
}
Comment on lines +94 to +101

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

When downloading configs for multiple nodes under the same account, combining the account label and node name and then truncating the entire string to 15 characters can easily lead to identical filenames if the account label is long (e.g., verylongaccountlabel-node1 and verylongaccountlabel-node2 both truncate to verylongaccount).

This causes two major issues:

  1. The user cannot distinguish which file belongs to which node.
  2. Browsers will automatically append suffixes like (1) to avoid overwriting (e.g., verylongaccount (1).conf), which contains spaces and parentheses. These characters are invalid under WireGuard's strict [a-zA-Z0-9_=+.-] pattern, causing the WireGuard app to reject the file upon import.

To prevent this, we should truncate the account label and node name individually before joining them, ensuring the total length remains within 15 characters.

function confFilename(label: string, node?: string): string {
  let stem = label.toLowerCase().replace(/[^a-z0-9._-]+/g, '-')
  if (node) {
    const nodeStem = node.toLowerCase().replace(/[^a-z0-9._-]+/g, '-')
    // Budget 15 chars: 9 for label, 1 for '-', 5 for node
    const labelPart = stem.slice(0, 9)
    const nodePart = nodeStem.slice(0, 5)
    stem = labelPart + '-' + nodePart
  } else if (stem.length > 15) {
    stem = stem.slice(0, 15)
  }
  // Trim AFTER truncating so a cut landing on a separator doesn't leave a
  // trailing "-"/"." right before the extension.
  stem = stem.replace(/^[-.]+|[-.]+$/g, '')
  return (stem === '' ? 'wgpanel' : stem) + '.conf'
}


// Mirrors the backend's peerOnlineWindow (180s) purely for display wording - the
// authoritative online/offline bit itself always comes from the server's `online`
// field, never recomputed here from last_handshake_at.
Expand Down Expand Up @@ -776,12 +789,11 @@ function AccountDetailDialog({
<Button
variant="secondary"
onClick={() => {
const slug = (configNodeName ?? 'node').toLowerCase().replace(/[^a-z0-9]+/g, '-')
const blob = new Blob([configText], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${account.label}-${slug}.conf`
link.download = confFilename(`${account.label}-${configNodeName ?? 'node'}`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Pass the account label and node name as separate arguments to the updated confFilename function to ensure they are truncated individually and fit within the 15-character limit without collisions.

Suggested change
link.download = confFilename(`${account.label}-${configNodeName ?? 'node'}`)
link.download = confFilename(account.label, configNodeName ?? 'node')

link.click()
URL.revokeObjectURL(url)
}}
Expand Down
Loading