Release 2.7.3.1: harden proxy recovery, updates and local API.

Restore orphaned system proxy after crash, require update SHA-256, add macOS /api auth token, fix UDP ping false positives, HTTPS-only subscriptions, and keep the UI responsive during connect.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
M4
2026-07-29 18:48:54 +03:00
co-authored by Cursor
parent dc700f2bac
commit 041cbb1250
32 changed files with 346 additions and 108 deletions
+39
View File
@@ -3,8 +3,10 @@ package config
import (
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"strings"
)
// Protocol identifies a VPN/proxy backend.
@@ -192,10 +194,47 @@ func (c *Config) Validate() error {
default:
return fmt.Errorf("config: profile %q: unsupported protocol %q", p.Name, p.Protocol)
}
for _, listen := range p.Listen {
if err := validateListenURI(listen); err != nil {
return fmt.Errorf("config: profile %q: %w", p.Name, err)
}
}
}
return nil
}
func validateListenURI(uri string) error {
uri = strings.TrimSpace(uri)
if uri == "" {
return nil
}
var hostPort string
var ok bool
for _, scheme := range []string{"http", "socks", "socks5", "https"} {
if hostPort, ok = parseListenHostPort(uri, scheme); ok {
break
}
}
if !ok {
lower := strings.ToLower(uri)
if strings.Contains(lower, "127.0.0.1") || strings.Contains(lower, "localhost") || strings.Contains(lower, "[::1]") {
return nil
}
return fmt.Errorf("listen %q must bind to loopback", uri)
}
host, _, err := net.SplitHostPort(hostPort)
if err != nil {
host = hostPort
}
host = strings.Trim(host, "[]")
switch strings.ToLower(host) {
case "127.0.0.1", "localhost", "::1", "":
return nil
default:
return fmt.Errorf("listen host %q is not loopback (only 127.0.0.1 / ::1)", host)
}
}
// ActiveProfile returns the selected profile.
func (c *Config) ActiveProfile() (*Profile, error) {
for i := range c.Profiles {
+11
View File
@@ -29,6 +29,17 @@ func (c *Config) ListProfiles() []ProfileInfo {
return out
}
// RedactProfileList clears Proxy URIs from a profile list (host/protocol remain).
// Use for periodic UI polls so credentials are not echoed for every profile.
func RedactProfileList(in []ProfileInfo) []ProfileInfo {
out := make([]ProfileInfo, len(in))
for i, p := range in {
out[i] = p
out[i].Proxy = ""
}
return out
}
func proxyHost(proxy string) string {
proxy = strings.TrimSpace(proxy)
if proxy == "" {