Release 1.3.1: fix hy2 UDP ping and shop link open.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -205,12 +205,18 @@
|
||||
}
|
||||
.shop-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
text-align: center;
|
||||
font: inherit;
|
||||
font-size: .78rem;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.shop-link:hover { text-decoration: underline; }
|
||||
|
||||
@@ -414,7 +420,7 @@
|
||||
<h3>Безопасное подключение</h3>
|
||||
<p>На любом устройстве — защита данных, стабильность и приватность. Демо-ключ от 30₽.</p>
|
||||
<button class="action primary" id="shopBtn" type="button">Купить на evilfox.win</button>
|
||||
<a class="shop-link" id="shopLink" href="https://evilfox.win/" rel="noopener">https://evilfox.win/</a>
|
||||
<button class="shop-link" id="shopLink" type="button">https://evilfox.win/</button>
|
||||
</section>
|
||||
<p class="ver" id="verLabel">Navis</p>
|
||||
</main>
|
||||
@@ -722,11 +728,16 @@
|
||||
}));
|
||||
|
||||
async function openShop(e) {
|
||||
if (e) e.preventDefault();
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
try {
|
||||
setMeta("Открываю evilfox.win…");
|
||||
await openURL(SHOP_URL);
|
||||
setMeta("Открыто в браузере", "ok");
|
||||
} catch (err) {
|
||||
setMeta(String(err), "err");
|
||||
setMeta("Не удалось открыть ссылку: " + String(err), "err");
|
||||
}
|
||||
}
|
||||
shopBtn.addEventListener("click", openShop);
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"vpnclient/internal/protocols/hysteria2"
|
||||
)
|
||||
|
||||
// Result is a TCP reachability measurement to a proxy host.
|
||||
// Result is a reachability measurement to a proxy host.
|
||||
type Result struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
@@ -22,12 +22,13 @@ type Result struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// PingProxyURI measures TCP connect time to the host in a share/proxy URI.
|
||||
// PingProxyURI measures connect time to the host in a share/proxy URI.
|
||||
func PingProxyURI(ctx context.Context, name, proxyURI string) Result {
|
||||
return PingProfile(ctx, name, "", proxyURI)
|
||||
}
|
||||
|
||||
// PingProfile pings using protocol hints when available.
|
||||
// Naive uses TCP; Hysteria 2 uses UDP (QUIC) — TCP would always look "refused".
|
||||
func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyURI string) Result {
|
||||
res := Result{Name: name}
|
||||
if strings.TrimSpace(proxyURI) == "" {
|
||||
@@ -35,8 +36,9 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
|
||||
return res
|
||||
}
|
||||
|
||||
hy2 := proto == config.ProtocolHysteria2 || hysteria2.Detect(proxyURI)
|
||||
var host, port string
|
||||
if proto == config.ProtocolHysteria2 || hysteria2.Detect(proxyURI) {
|
||||
if hy2 {
|
||||
h, p, err := hysteria2.HostPort(proxyURI)
|
||||
if err != nil {
|
||||
res.Error = err.Error()
|
||||
@@ -49,7 +51,6 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
|
||||
res.Error = err.Error()
|
||||
return res
|
||||
}
|
||||
// Parse host from normalized URI without importing net/url dance twice
|
||||
rest := normalized
|
||||
if i := strings.Index(rest, "://"); i >= 0 {
|
||||
rest = rest[i+3:]
|
||||
@@ -75,15 +76,76 @@ func PingProfile(ctx context.Context, name string, proto config.Protocol, proxyU
|
||||
return res
|
||||
}
|
||||
|
||||
d := net.Dialer{Timeout: 4 * time.Second}
|
||||
start := time.Now()
|
||||
conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, port))
|
||||
var err error
|
||||
if hy2 {
|
||||
err = probeUDP(ctx, host, port)
|
||||
} else {
|
||||
err = probeTCP(ctx, host, port)
|
||||
}
|
||||
if err != nil {
|
||||
res.Error = fmt.Sprintf("недоступен: %v", err)
|
||||
res.Error = friendlyDialError(err, hy2)
|
||||
return res
|
||||
}
|
||||
_ = conn.Close()
|
||||
res.Ms = time.Since(start).Milliseconds()
|
||||
res.OK = true
|
||||
return res
|
||||
}
|
||||
|
||||
func probeTCP(ctx context.Context, host, port string) error {
|
||||
d := net.Dialer{Timeout: 4 * time.Second}
|
||||
conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func probeUDP(ctx context.Context, host, port string) error {
|
||||
d := net.Dialer{Timeout: 4 * time.Second}
|
||||
conn, err := d.DialContext(ctx, "udp", net.JoinHostPort(host, port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
deadline := time.Now().Add(1500 * time.Millisecond)
|
||||
if dl, ok := ctx.Deadline(); ok && dl.Before(deadline) {
|
||||
deadline = dl
|
||||
}
|
||||
_ = conn.SetDeadline(deadline)
|
||||
|
||||
// Any datagram is enough to exercise the UDP path; QUIC rarely answers a bare probe.
|
||||
if _, err := conn.Write([]byte{0}); err != nil {
|
||||
return err
|
||||
}
|
||||
buf := make([]byte, 64)
|
||||
_, err = conn.Read(buf)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
// No ICMP unreachable → port is typically open or filtered; treat as reachable for UI ping.
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func friendlyDialError(err error, hy2 bool) string {
|
||||
msg := err.Error()
|
||||
lower := strings.ToLower(msg)
|
||||
switch {
|
||||
case strings.Contains(lower, "refused"):
|
||||
if hy2 {
|
||||
return "UDP порт недоступен (connection refused)"
|
||||
}
|
||||
return "порт закрыт (connection refused)"
|
||||
case strings.Contains(lower, "timeout") || strings.Contains(lower, "timed out"):
|
||||
return "таймаут"
|
||||
case strings.Contains(lower, "no such host"):
|
||||
return "DNS: хост не найден"
|
||||
default:
|
||||
return fmt.Sprintf("недоступен: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package netcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"vpnclient/internal/config"
|
||||
)
|
||||
|
||||
func TestFriendlyDialError(t *testing.T) {
|
||||
err := &netError{s: "dial tcp 1.2.3.4:22514: connectex: No connection could be made because the target machine actively refused it."}
|
||||
got := friendlyDialError(err, true)
|
||||
if !strings.Contains(got, "UDP") {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
type netError struct{ s string }
|
||||
|
||||
func (e *netError) Error() string { return e.s }
|
||||
func (e *netError) Timeout() bool { return false }
|
||||
func (e *netError) Temporary() bool { return false }
|
||||
|
||||
func TestPingProfileEmpty(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
r := PingProfile(ctx, "x", config.ProtocolHysteria2, "")
|
||||
if r.OK || r.Error == "" {
|
||||
t.Fatalf("%+v", r)
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func Fetch(ctx context.Context, rawURL string) ([]Item, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Navis/1.3.0")
|
||||
req.Header.Set("User-Agent", "Navis/1.3.1")
|
||||
client := &http.Client{Timeout: 45 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
// CurrentVersion is the shipped client version.
|
||||
const CurrentVersion = "1.3.0"
|
||||
const CurrentVersion = "1.3.1"
|
||||
|
||||
// DefaultManifestURL is the update feed (hosted in the project git repo).
|
||||
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
|
||||
|
||||
Reference in New Issue
Block a user