Release 4.2.0+1: store-compliance формулировки и UX — убраны все цены/продажи из приложения (оффер «3 дня бесплатно» без ₽), «Активировать ключ» → «Добавить конфигурацию» (URL-ссылка, placeholder https://), блок «Ключ доступа» заменён статусом «Конфигурация добавлена/не добавлена» (URL не отображается), глобально Подписка→Конфигурация/Ваучер→URL-ссылка/Тариф→Профиль в UI и Go-строках; большая круглая кнопка ПОДКЛЮЧИТЬСЯ/ОТКЛЮЧИТЬСЯ со статусом, убран футер «Готово к подключению», протокол в списке — буквенный бейдж только при отличии от активного, трафик «ДОСТУПНО X ГБ» + «Активен до DD.MM.YYYY» + процент; пинг отдельного сервера кнопкой в строке, экран Статистика (за сегодня: трафик/время в сети/сессии + график 7 дней из localStorage), Kill Switch для VPN-режима (blackhole-маршруты сохраняются при обрыве туннеля, для прокси — системный прокси остаётся на мёртвый порт), тумблер «Авто-лучший сервер» в Настройках. Windows 4.2.0.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-08-01 22:34:43 +03:00
co-authored by Cursor
parent 74ffbd87d7
commit 2a57987aea
22 changed files with 670 additions and 127 deletions
+4 -1
View File
@@ -25,5 +25,8 @@ type Status struct {
SOCKSProxy string `json:"socks_proxy,omitempty"`
SystemProxy bool `json:"system_proxy"`
VPNActive bool `json:"vpn_active,omitempty"`
Extra map[string]string `json:"extra,omitempty"`
// KillSwitchActive: the tunnel died unexpectedly and traffic is being
// blocked on purpose until manual disconnect / reconnect.
KillSwitchActive bool `json:"kill_switch_active,omitempty"`
Extra map[string]string `json:"extra,omitempty"`
}
+83 -2
View File
@@ -39,6 +39,12 @@ type Manager struct {
// vpn is the active VPN-mode (TUN) session, nil in proxy mode.
vpn *vpnmode.Session
// watchGen invalidates engine watchdogs from previous connects.
watchGen uint64
// ksEngaged is true after an unexpected engine death with kill switch on:
// blackhole routes / dead system proxy are intentionally left in place.
ksEngaged bool
}
func NewManager(cfgPath string, cfg *config.Config, stderr io.Writer) (*Manager, error) {
@@ -66,6 +72,18 @@ func (m *Manager) Connect(ctx context.Context, profileName string) error {
return fmt.Errorf("already connected; disconnect first")
}
// Reconnect releases a previously engaged kill switch (blackhole state).
if m.ksEngaged {
if m.vpn != nil {
_ = m.vpn.Close()
m.vpn = nil
}
if m.sys.Enabled() {
_ = m.sys.Disable()
}
m.ksEngaged = false
}
if profileName != "" {
m.cfg.Active = profileName
}
@@ -135,9 +153,70 @@ func (m *Manager) Connect(ctx context.Context, profileName string) error {
m.engine = engine
m.profile = profile
m.watchGen++
go m.watchEngine(m.watchGen, engine)
return nil
}
// watchEngine polls the engine and reacts to an UNEXPECTED death (crash of the
// core process). A normal Disconnect bumps watchGen first, so the watcher just
// exits. With kill switch enabled the blackhole state is kept, otherwise the
// network is restored (system proxy off, TUN routes removed).
func (m *Manager) watchEngine(gen uint64, engine Engine) {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for range ticker.C {
m.mu.Lock()
if m.watchGen != gen || m.engine != engine {
m.mu.Unlock()
return
}
if engine.Running() {
m.mu.Unlock()
continue
}
// Unexpected death: engine stopped without Disconnect.
m.engine = nil
m.profile = nil
ks := m.cfg.KillSwitch
vpn := m.vpn
sysOn := m.sys.Enabled()
if ks && (vpn != nil || sysOn) {
// Keep TUN blackhole routes / dead system proxy: traffic is
// blocked until the user disconnects manually or reconnects.
m.ksEngaged = true
m.mu.Unlock()
fmt.Fprintf(m.stderr, "kill switch: соединение прервано — трафик заблокирован до ручного отключения или переподключения\n")
return
}
m.vpn = nil
m.mu.Unlock()
if vpn != nil {
_ = vpn.Close()
}
if sysOn {
_ = m.sys.Disable()
}
fmt.Fprintf(m.stderr, "соединение прервано — сетевые настройки восстановлены\n")
return
}
}
// KillSwitchEnabled returns the persisted kill switch setting.
func (m *Manager) KillSwitchEnabled() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.cfg.KillSwitch
}
// SetKillSwitch persists the kill switch setting.
func (m *Manager) SetKillSwitch(enabled bool) error {
m.mu.Lock()
defer m.mu.Unlock()
m.cfg.KillSwitch = enabled
return config.Save(m.cfgPath, *m.cfg)
}
func (m *Manager) Disconnect() error {
m.mu.Lock()
sys := m.sys
@@ -146,6 +225,8 @@ func (m *Manager) Disconnect() error {
m.engine = nil
m.profile = nil
m.vpn = nil
m.watchGen++ // cancel the engine watchdog — this stop is intentional
m.ksEngaged = false
m.mu.Unlock()
var first error
@@ -181,7 +262,7 @@ func (m *Manager) Status() Status {
m.mu.Lock()
defer m.mu.Unlock()
st := Status{SystemProxy: m.sys.Enabled(), VPNActive: m.vpn != nil}
st := Status{SystemProxy: m.sys.Enabled(), VPNActive: m.vpn != nil, KillSwitchActive: m.ksEngaged}
if m.engine == nil || !m.engine.Running() {
return st
}
@@ -608,7 +689,7 @@ func (m *Manager) applySubscription(ctx context.Context, res *subscription.Resul
return ImportResult{Skipped: res.Skipped, Warnings: res.Warnings},
fmt.Errorf("нет валидных серверов: %d записей пропущено (%s)", res.Skipped, strings.Join(res.Warnings, "; "))
}
return ImportResult{}, fmt.Errorf(подписке нет поддерживаемых ссылок (naive / hysteria2 / awg / vless / vmess / trojan)")
return ImportResult{}, fmt.Errorf(конфигурации нет поддерживаемых ссылок (naive / hysteria2 / awg / vless / vmess / trojan)")
}
// Subscription usage info: header data first, enriched via Remnawave API