Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fd29fa5b2 | ||
|
|
dc3e9beb14 | ||
|
|
9d7d4c8287 | ||
|
|
469106d502 |
@@ -11,7 +11,7 @@ GO = go
|
||||
CGO_ENABLED = 1
|
||||
WINDRES ?= x86_64-w64-mingw32-windres
|
||||
MINGW_CC ?= x86_64-w64-mingw32-gcc
|
||||
SEMVER ?= 0.6.7
|
||||
SEMVER ?= 0.6.9
|
||||
GIT_HASH = $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
VERSION = $(SEMVER)-$(GIT_HASH)
|
||||
LDFLAGS = -s -w -X lmvpn/internal/version.Version=$(VERSION)
|
||||
|
||||
@@ -99,13 +99,13 @@ func raceDial(ctx context.Context, network string, ips []string, port string) (n
|
||||
go func(target string) {
|
||||
d := &net.Dialer{}
|
||||
c, err := d.DialContext(raceCtx, network, net.JoinHostPort(target, port))
|
||||
select {
|
||||
case resultCh <- result{conn: c, err: err}:
|
||||
case <-raceCtx.Done():
|
||||
if c != nil {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
// Always send the result: resultCh is buffered (len(ips)),
|
||||
// so this never blocks. Do NOT use a select with
|
||||
// <-raceCtx.Done() here - when the context is cancelled
|
||||
// both cases would be ready and Go's random select pick
|
||||
// could skip the send, causing the drain loop below to
|
||||
// block forever.
|
||||
resultCh <- result{conn: c, err: err}
|
||||
}(ip)
|
||||
}
|
||||
|
||||
@@ -117,7 +117,8 @@ func raceDial(ctx context.Context, network string, ips []string, port string) (n
|
||||
// any late successful connections.
|
||||
cancel()
|
||||
for j := i + 1; j < len(ips); j++ {
|
||||
if late := <-resultCh; late.conn != nil {
|
||||
late := <-resultCh
|
||||
if late.conn != nil {
|
||||
late.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestRaceDialNoDeadlock is a regression test for a select race
|
||||
// condition that caused raceDial to deadlock ~50% of the time when
|
||||
// multiple IPs were raced and the first succeeded. The bug was a
|
||||
// select with two simultaneously-ready cases (send result vs
|
||||
// <-raceCtx.Done()); Go's random selection could skip the send,
|
||||
// leaving the drain loop blocked forever.
|
||||
//
|
||||
// We run many iterations because the bug was probabilistic.
|
||||
func TestRaceDialNoDeadlock(t *testing.T) {
|
||||
const iterations = 200
|
||||
|
||||
for n := 0; n < iterations; n++ {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Accept both connections in background (both IPs dial the
|
||||
// same listener since raceDial uses a single port).
|
||||
go func() {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
port := portOf(ln.Addr())
|
||||
ips := []string{"127.0.0.1", "127.0.0.1"}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
conn, err := raceDial(ctx, "tcp", ips, port)
|
||||
cancel()
|
||||
if err == nil && conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
ln.Close()
|
||||
|
||||
// If we get here without the 5s timeout, the iteration passed.
|
||||
// A deadlock would trigger the test-wide 60s timeout.
|
||||
}
|
||||
|
||||
// If we reach here, no iteration deadlocked.
|
||||
}
|
||||
|
||||
// TestRaceDialAllFail verifies that when all dials fail, raceDial
|
||||
// returns an error instead of blocking.
|
||||
func TestRaceDialAllFail(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Port 1: connection refused on most systems.
|
||||
ips := []string{"127.0.0.1", "127.0.0.1"}
|
||||
_, err := raceDial(ctx, "tcp", ips, "1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error when all dials fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRaceDialSingleIP verifies the single-IP path still works.
|
||||
func TestRaceDialSingleIP(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
c, _ := ln.Accept()
|
||||
if c != nil {
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
conn, err := raceDial(ctx, "tcp", []string{"127.0.0.1"}, portOf(ln.Addr()))
|
||||
if err != nil {
|
||||
t.Fatalf("raceDial single IP: %v", err)
|
||||
}
|
||||
conn.Close()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// TestRaceDialContextCancelled ensures raceDial returns promptly when
|
||||
// the parent context is cancelled while dials are in flight.
|
||||
func TestRaceDialContextCancelled(t *testing.T) {
|
||||
// Dial a non-routable address so the dial hangs until context
|
||||
// cancellation.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := raceDial(ctx, "tcp", []string{"10.255.255.1", "10.255.255.2"}, "80")
|
||||
done <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err == nil {
|
||||
t.Fatal("expected error on cancelled context")
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("raceDial did not return after context cancellation")
|
||||
}
|
||||
}
|
||||
|
||||
// portOf extracts the port from a net.Addr.
|
||||
func portOf(addr net.Addr) string {
|
||||
_, port, _ := net.SplitHostPort(addr.String())
|
||||
return port
|
||||
}
|
||||
@@ -130,7 +130,6 @@ func Run() {
|
||||
fyne.Do(func() {
|
||||
a.windowHidden = false
|
||||
activateApp()
|
||||
showDockIcon()
|
||||
a.window.Show()
|
||||
a.window.RequestFocus()
|
||||
})
|
||||
@@ -149,7 +148,6 @@ func Run() {
|
||||
if a.windowHidden {
|
||||
a.windowHidden = false
|
||||
activateApp()
|
||||
showDockIcon()
|
||||
fyne.Do(func() {
|
||||
if a.windowHidden {
|
||||
return
|
||||
@@ -173,7 +171,6 @@ func Run() {
|
||||
a.window.SetCloseIntercept(func() {
|
||||
if cfg.CloseToTray {
|
||||
a.windowHidden = true
|
||||
hideDockIcon()
|
||||
a.window.Hide()
|
||||
} else {
|
||||
a.quit()
|
||||
|
||||
@@ -25,12 +25,6 @@ static BOOL appShouldHandleReopen(id self, SEL cmd, id sender, BOOL flag) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
static void setDockIconVisible(int visible) {
|
||||
Class cls = objc_getClass("NSApplication");
|
||||
id app = ((id (*)(Class, SEL))objc_msgSend)(cls, sel_getUid("sharedApplication"));
|
||||
((void (*)(id, SEL, long))objc_msgSend)(app, sel_getUid("setActivationPolicy:"), visible ? 0 : 1);
|
||||
}
|
||||
|
||||
static void cmActivateApp(void) {
|
||||
Class cls = objc_getClass("NSApplication");
|
||||
id app = ((id (*)(Class, SEL))objc_msgSend)(cls, sel_getUid("sharedApplication"));
|
||||
@@ -53,9 +47,6 @@ static void cmRegisterReopenHandler(void) {
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func showDockIcon() { C.setDockIconVisible(1) }
|
||||
func hideDockIcon() { C.setDockIconVisible(0) }
|
||||
|
||||
func activateApp() { C.cmActivateApp() }
|
||||
|
||||
func registerReopenHandler() { C.cmRegisterReopenHandler() }
|
||||
|
||||
@@ -4,9 +4,6 @@ package ui
|
||||
|
||||
var onAppActive func()
|
||||
|
||||
func showDockIcon() {}
|
||||
func hideDockIcon() {}
|
||||
|
||||
func activateApp() {}
|
||||
|
||||
func registerReopenHandler() {}
|
||||
|
||||
@@ -63,7 +63,6 @@ func (a *App) setupTray() {
|
||||
fyne.NewMenuItem(i18n.T("TrayShowWindow"), func() {
|
||||
a.windowHidden = false
|
||||
activateApp()
|
||||
showDockIcon()
|
||||
a.window.Show()
|
||||
a.window.RequestFocus()
|
||||
}),
|
||||
|
||||
@@ -197,7 +197,7 @@ func (sm *SessionManager) run(ctx context.Context, cfg SessionConfig) {
|
||||
// to avoid consuming the server's 30s ReadyTimeout budget.
|
||||
var beforeCIDRs []string
|
||||
allURLSources := append(append([]model.CIDRURLSource{}, cfg.CIDRV4URLs...), cfg.CIDRV6URLs...)
|
||||
if len(allURLSources) > 0 {
|
||||
if len(allURLSources) > 0 && cfg.RoutingMode != route.ModeFull {
|
||||
sm.stats.SetConnectStep("fetch_cidrs")
|
||||
sm.setState(stats.StateConnecting)
|
||||
log.L().Info("fetching before-proxy CIDR lists", "url_count", len(allURLSources))
|
||||
@@ -588,6 +588,9 @@ func (sm *SessionManager) setupTUN(init protocol.InitMessage, cfg SessionConfig,
|
||||
// (via the tunnel) and dynamically adds their routes to the route
|
||||
// manager. This is called in a goroutine after the data plane is up.
|
||||
func (sm *SessionManager) fetchAfterProxyCIDRs(ctx context.Context, cfg SessionConfig) {
|
||||
if cfg.RoutingMode == route.ModeFull {
|
||||
return
|
||||
}
|
||||
allURLSources := append(append([]model.CIDRURLSource{}, cfg.CIDRV4URLs...), cfg.CIDRV6URLs...)
|
||||
// Count only "after" sources for logging.
|
||||
afterCount := 0
|
||||
@@ -659,6 +662,10 @@ func (sm *SessionManager) RefreshCIDRs() {
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.RoutingMode == route.ModeFull {
|
||||
return
|
||||
}
|
||||
|
||||
// Use a background context with 30s timeout so the refresh works
|
||||
// even if the session context is in a weird state.
|
||||
refreshCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>LSUIElement</key>
|
||||
<false/>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
|
||||
Reference in New Issue
Block a user