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:
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user