Skip to content
Open
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@
## 2026-04-21 - In-Place Two-Pointer URL Decoding
**Learning:** In recursive or multi-pass percent-decoding routines like `url_decode_complete`, allocating one reusable secondary output `Vec<u8>` before the pass loop and swapping the two buffers on each pass introduces redundant heap buffer management overhead. Because percent decoding is monotonically non-expanding (a 3-byte `%XX` decodes to 1 byte, while literal bytes remain 1 byte), decoding in-place within a single `Vec<u8>` using read and write indices (`write <= read`) cuts heap buffer allocations by 50% and improves decoding latency by ~15-25% (~270ns down to ~228ns median in `url_decode`).
**Action:** In string/byte decoding algorithms where decoded bytes are equal to or shorter than the encoded representation, perform modifications in-place within a single mutable buffer using two-pointer traversal instead of ping-ponging allocations across multiple vectors.
## 2026-04-20 - Fast-Path Colon Pre-Check & Borrowed Anchor Names in YAML Line Scanners
**Learning:** In line-by-line YAML scanners (`mapping_colon_index`), adding `if !body.contains(':') { return None; }` allows fast-path SIMD bailing before executing character state machine loops over non-mapping lines (such as rules and array items). In flow collections, anchor and alias names can be borrowed slices of the input instead of freshly allocated strings, so the anchor table keeps hashed lookup with `HashSet<&'a str>`. A companion change that replaced those hash sets with `Vec` linear scans was reverted after review: the sets grow with the input document, so linear scans turn extraction quadratic on large mappings (the `[_; 16]` bound is only the flow depth, not the key count), and `Vec` still allocates on its first push.
**Action:** In YAML line scanners, pre-check for required delimiter characters (`:`) before running quote-tracking loops, borrow name slices instead of allocating strings where the input outlives the scan, and keep hash-based tracking for any collection that grows with the input document.
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@
**Vulnerability:** `override_update` and `override_create` allowed persisting arbitrary URL strings (including unsupported schemes like `file://` or `javascript:` and oversized strings) directly into override metadata without scheme or length validation.
**Learning:** Metadata update commands that bypass network download routines can easily omit URL scheme and length checks performed by network fetchers.
**Prevention:** Always enforce centralized URL validation (`http`/`https` scheme check and length cap) across all IPC handlers that receive or update URL parameters.
## 2026-07-19 - Unvalidated User-Agent Input in IPC Commands
**Vulnerability:** `update_subscription_user_agent` accepted arbitrary string inputs over IPC without length or control character checks. Passing newlines (`\r\n`) or oversized strings persisted invalid header values into settings, causing HTTP client building during subscription downloads to fail permanently and leading to persistent DoS.
**Learning:** Setting values that map directly to HTTP request headers must be validated for length limits and control characters prior to persistence.
**Prevention:** Always validate User-Agent strings (`validate_user_agent`) to enforce maximum length limits (<= 512 bytes) and reject control characters (`\r`, `\n`, `\0`) before writing to settings or constructing HTTP clients.
2 changes: 2 additions & 0 deletions apps/desktop/src-tauri/src/backend_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ pub mod codes {
pub const SUB_DIRECT_DOWNLOAD_FAILED: u16 = 2015;
/// Retrying subscription fetch through local proxy.
pub const SUB_PROXY_RETRY: u16 = 2016;
/// A saved User-Agent failed validation and was replaced with the default.
pub const SUB_UA_INVALID: u16 = 2017;

// Prism: 3000-3999
pub const PRISM_APPLY_FAILED: u16 = 3001;
Expand Down
22 changes: 20 additions & 2 deletions apps/desktop/src-tauri/src/core/subscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,28 @@ fn build_http_client_with_proxy(
client_builder = client_builder.resolve(host, *addr);
}

// Determine User-Agent: use provided UA, or default to Zephyr
// Determine User-Agent: use provided UA if valid, or default to Zephyr
let ua_to_use = match user_agent {
Some(ua) if !ua.trim().is_empty() => ua.trim().to_owned(),
Some(ua)
if !ua.trim().is_empty()
&& zephyr_core::config::subscription::validate_user_agent(ua.trim()).is_ok() =>
{
ua.trim().to_owned()
}
_ => {
// A saved UA that no longer passes validation (e.g. over the
// 512-byte cap) used to be sent as-is; it is now replaced, so
// say so instead of silently swapping in the default on every
// download.
if let Some(ua) = user_agent {
if !ua.trim().is_empty() {
crate::emit_warn!(
Subscription,
SUB_UA_INVALID,
"Saved User-Agent failed validation, using default Zephyr User-Agent"
);
}
}
// Default Zephyr User-Agent with version
let version = env!("CARGO_PKG_VERSION");
format!("Zephyr/{version}")
Expand Down
11 changes: 10 additions & 1 deletion apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1281,8 +1281,17 @@ fn update_subscription_user_agent(
app: tauri::AppHandle,
user_agent: Option<String>,
) -> Result<(), String> {
if let Some(ua) = user_agent.as_deref() {
let trimmed = ua.trim();
if !trimmed.is_empty() {
zephyr_core::config::subscription::validate_user_agent(trimmed)
.map_err(|e| e.to_string())?;
}
}
commit_settings(&app, |s| {
s.subscription_user_agent = user_agent.filter(|ua| !ua.is_empty());
s.subscription_user_agent = user_agent
.map(|ua| ua.trim().to_owned())
.filter(|ua| !ua.is_empty());
})
}

Expand Down
28 changes: 28 additions & 0 deletions apps/desktop/src/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@ export const translations = {
fakeClientType: "Client Type",
fakeClientCustomUA: "Custom User-Agent",
fakeClientWarning: "Warning: Disabling this may cause incorrect config format from subscriptions.",
userAgentRejected: "Custom User-Agent was rejected and has been reverted to the saved value.",
userAgentSyncFailed: "Failed to sync User-Agent; your edit is kept and will retry on the next change.",
custom: "Custom...",
advancedSubmenu: "Settings Submenu",
dnsRewrite: "DNS Leak Protection",
Expand Down Expand Up @@ -1517,6 +1519,8 @@ export const translations = {
fakeClientType: "客户端类型",
fakeClientCustomUA: "自定义 User-Agent",
fakeClientWarning: "警告:关闭此项可能导致部分机场返回不兼容的订阅格式",
userAgentRejected: "自定义 User-Agent 被拒绝,已恢复为保存的值。",
userAgentSyncFailed: "User-Agent 同步失败,你的修改已保留,下次修改时会重试。",
custom: "自定义...",
advancedSubmenu: "设置子菜单",
dnsRewrite: "DNS 防泄漏",
Expand Down Expand Up @@ -2438,6 +2442,8 @@ export const translations = {
fakeClientType: "クライアントの種類",
fakeClientCustomUA: "カスタム User-Agent",
fakeClientWarning: "警告:これを無効にすると、一部のプロバイダーが互換性のない形式を返す可能性があります。",
userAgentRejected: "カスタム User-Agent は拒否され、保存された値に戻されました。",
userAgentSyncFailed: "User-Agent の同期に失敗しました。編集内容は保持され、次回変更時に再試行されます。",
general: "全般",
geoData: "Geo データベース",
geoDataDesc: "GeoIP と GeoSite ルーティングデータベースを更新",
Expand Down Expand Up @@ -3466,6 +3472,8 @@ export const translations = {
fakeClientType: "클라이언트 유형",
fakeClientCustomUA: "사용자 지정 User-Agent",
fakeClientWarning: "경고: 이를 해제하면 일부 공급자가 호환되지 않는 형식을 반환할 수 있습니다.",
userAgentRejected: "사용자 지정 User-Agent가 거부되어 저장된 값으로 되돌아갔습니다.",
userAgentSyncFailed: "User-Agent 동기화에 실패했습니다. 편집 내용은 유지되며 다음 변경 시 다시 시도합니다.",
general: "일반",
geoData: "Geo 데이터베이스",
geoDataDesc: "GeoIP 및 GeoSite 라우팅 데이터베이스 업데이트",
Expand Down Expand Up @@ -4551,6 +4559,8 @@ export const translations = {
fakeClientType: "Тип клиента",
fakeClientCustomUA: "Свой User-Agent",
fakeClientWarning: "Предупреждение: отключение может привести к неверному формату конфигурации от подписок.",
userAgentRejected: "Пользовательский User-Agent отклонён, восстановлено сохранённое значение.",
userAgentSyncFailed: "Не удалось синхронизировать User-Agent; ваши изменения сохранены и будут повторены при следующем изменении.",
custom: "Свой...",
advancedSubmenu: "Подменю настроек",
dnsRewrite: "DNS Rewrite (антиутечка и Fake-IP)",
Expand Down Expand Up @@ -5574,6 +5584,8 @@ export const translations = {
fakeClientType: "Tipo de cliente",
fakeClientCustomUA: "User-Agent personalizado",
fakeClientWarning: "Advertencia: Desactivar esto puede causar formatos de configuración incorrectos.",
userAgentRejected: "El User-Agent personalizado fue rechazado y se restauró el valor guardado.",
userAgentSyncFailed: "No se pudo sincronizar el User-Agent; tu edición se conserva y se reintentará en el próximo cambio.",
custom: "Personalizado...",
advancedSubmenu: "Submenú de ajustes",
dnsRewrite: "DNS Rewrite (antifuga y Fake-IP)",
Expand Down Expand Up @@ -6597,6 +6609,8 @@ export const translations = {
fakeClientType: "نوع کلاینت",
fakeClientCustomUA: "User-Agent سفارشی",
fakeClientWarning: "هشدار: غیرفعال‌سازی این ممکن است باعث فرمت نادرست پیکربندی از اشتراک‌ها شود.",
userAgentRejected: "User-Agent سفارشی رد شد و مقدار ذخیره‌شده بازیابی شد.",
userAgentSyncFailed: "همگام‌سازی User-Agent ناموفق بود؛ ویرایش شما حفظ شده و در تغییر بعدی دوباره تلاش می‌شود.",
custom: "سفارشی...",
advancedSubmenu: "زیرمنوی تنظیمات",
dnsRewrite: "بازنویسی DNS (ضد نشت و Fake-IP)",
Expand Down Expand Up @@ -7620,6 +7634,8 @@ export const translations = {
fakeClientType: "Müşderi görnüşi",
fakeClientCustomUA: "Ýörite User-Agent",
fakeClientWarning: "Duýduryş: Muny öçürmek abunalardan nädogry konfigurasiýa formatyna sebäp bolup biler.",
userAgentRejected: "Ýörite User-Agent ret edildi we saklanan baha dikeldildi.",
userAgentSyncFailed: "User-Agent sinhronlaşdyrylyp bilinmedi; üýtgetmäňiz saklandy we indiki üýtgetmede gaýtadan synanyşylar.",
custom: "Hususy...",
advancedSubmenu: "Sazlamalar altmenyusy",
dnsRewrite: "DNS Rewrite (akyp çykmagyň öňünden we Fake-IP)",
Expand Down Expand Up @@ -8581,6 +8597,8 @@ export const translations = {
fakeClientType: "Client အမျိုးအစား",
fakeClientCustomUA: "စိတ်ကြိုက် User-Agent",
fakeClientWarning: "သတိပေးချက်: ဤသည်ကို ပိတ်လျှင် စာရင်းသွင်းမှုများမှ မမှန်ကန်သော ဖိုင်ပုံစံ ရရှိနိုင်သည်။",
userAgentRejected: "စိတ်ကြိုက် User-Agent ကို ငြင်းပယ်ခဲ့ပြီး သိမ်းဆည်းထားသော တန်ဖိုးသို့ ပြန်လည်ရောက်ရှိခဲ့သည်။",
userAgentSyncFailed: "User-Agent ထပ်တူပြုလုပ်ခြင်း မအောင်မြင်ပါ။ သင်၏တည်းဖြတ်မှုကို ထိန်းသိမ်းထားပြီး နောက်ထပ်ပြောင်းလဲမှုတွင် ထပ်မံစမ်းသပ်မည်။",
general: "ယေဘုယျ",
geoData: "Geo ဒေတာဘေ့စ်များ",
geoDataDesc: "GeoIP နှင့် GeoSite လမ်းကြောင်း ဒေတာဘေ့စ်များ အပ်ဒိတ်လုပ်ရန်",
Expand Down Expand Up @@ -9666,6 +9684,8 @@ export const translations = {
fakeClientType: "نوع العميل",
fakeClientCustomUA: "User-Agent مخصص",
fakeClientWarning: "تحذير: قد يؤدي إيقاف هذا إلى الحصول على تهيئة غير صحيحة من الاشتراكات.",
userAgentRejected: "تم رفض User-Agent المخصص واستعادة القيمة المحفوظة.",
userAgentSyncFailed: "فشل مزامنة User-Agent؛ تم الاحتفاظ بتعديلك وستتم إعادة المحاولة عند التغيير التالي.",
custom: "مخصص...",
advancedSubmenu: "قائمة الإعدادات الفرعية",
dnsRewrite: "إعادة كتابة DNS (منع التسرب و Fake-IP)",
Expand Down Expand Up @@ -10689,6 +10709,8 @@ export const translations = {
fakeClientType: "کلائنٹ کی قسم",
fakeClientCustomUA: "حسب ضرورت User-Agent",
fakeClientWarning: "تنبیہ: اسے بند کرنے سے سبسکرپشنز سے غلط تشکیل آ سکتی ہے۔",
userAgentRejected: "حسب ضرورت User-Agent مسترد کر دیا گیا ہے اور محفوظ شدہ قدر بحال کر دی گئی ہے۔",
userAgentSyncFailed: "User-Agent کی ہم آہنگی ناکام ہو گئی؛ آپ کی ترمیم محفوظ ہے اور اگلی تبدیلی پر دوبارہ کوشش کی جائے گی۔",
custom: "custom...",
advancedSubmenu: "ترتیبات سب مینو",
dnsRewrite: "DNS دوبارہ تحریر (رسائی روکیں اور Fake-IP)",
Expand Down Expand Up @@ -11723,6 +11745,8 @@ export const translations = {
fakeClientType: "Aina ya mteja",
fakeClientCustomUA: "User-Agent maalum",
fakeClientWarning: "Onyo: Kuzima hii kunaweza kusababisha muundo batili wa usanidi kutoka usajili.",
userAgentRejected: "User-Agent maalum ilikataliwa na thamani iliyohifadhiwa imerejeshwa.",
userAgentSyncFailed: "Kushindwa kusawazisha User-Agent; uhariri wako umehifadhiwa na utajaribiwa tena kwenye mabadiliko yajayo.",
custom: "Desturi...",
advancedSubmenu: "Menyu ndogo ya Mipangilio",
dnsRewrite: "Kuandika upya DNS (Anti-leak & Fake-IP)",
Expand Down Expand Up @@ -12796,6 +12820,8 @@ export const translations = {
fakeClientType: "İstemci türü",
fakeClientCustomUA: "Özel User-Agent",
fakeClientWarning: "Uyarı: Bunu devre dışı bırakmak aboneliklerden hatalı yapılandırma biçimine neden olabilir.",
userAgentRejected: "Özel User-Agent reddedildi ve kayıtlı değere geri dönüldü.",
userAgentSyncFailed: "User-Agent eşitlenemedi; düzenlemeniz korundu ve bir sonraki değişiklikte yeniden denenecek.",
custom: "Özel...",
advancedSubmenu: "Ayarlar Alt Menüsü",
dnsRewrite: "DNS Yeniden Yazma (Sızıntı Önleme & Fake-IP)",
Expand Down Expand Up @@ -13869,6 +13895,8 @@ export const translations = {
fakeClientType: "Loại client",
fakeClientCustomUA: "User-Agent tùy chỉnh",
fakeClientWarning: "Cảnh báo: Tắt tính năng này có thể gây sai định dạng cấu hình từ đăng ký.",
userAgentRejected: "User-Agent tùy chỉnh đã bị từ chối và đã khôi phục về giá trị đã lưu.",
userAgentSyncFailed: "Đồng bộ User-Agent thất bại; chỉnh sửa của bạn được giữ lại và sẽ thử lại ở lần thay đổi tiếp theo.",
custom: "Tùy chỉnh...",
advancedSubmenu: "Menu phụ cài đặt",
dnsRewrite: "Ghi đè DNS (Chống rò rỉ & Fake-IP)",
Expand Down
Loading
Loading