From 570e37a66b8a2be62b587bf0a2a0e686ae6b4a1b Mon Sep 17 00:00:00 2001 From: badoriie Date: Wed, 27 May 2026 21:36:15 +0200 Subject: [PATCH 1/2] Add linux/386 build target for iSH on iOS iSH (https://ish.app/) is a Linux x86 emulator for iOS that lets volunteers run server-side software on an iPhone. This adds first-class support for running a Conduit node on iSH, opening up Conduit to people who only have an iOS device and want to help users in restricted countries where the iOS app is not yet on the App Store. Changes: - New `build-linux-386` Makefile target producing statically linked x86 32-bit binaries (conduit-linux-386, conduit-monitor-linux-386) - Built with GO386=softfloat: iSH emulates SSE2 instructions in software; softfloat makes Go emit simpler integer instructions instead, reducing emulation overhead - Built with netgo tag: uses Go's pure-Go DNS resolver, avoiding glibc DNS syscall overhead under iSH's emulation layer - GOMAXPROCS=1 set at startup (cmd/ish_linux_386.go): iSH's %gs thread-local storage emulation is unreliable in signal handler context. With multiple Ps, Go sends SIGURG to preempt goroutines across OS threads, triggering a "bad g in signal handler" crash. Keeping all goroutines on a single P prevents this. - conduit-linux-386 added to release artifact uploads and GitHub release assets in the CLI release workflow - GO_BUILD and GO_BUILD_MONITOR macros extended with an optional extra-env parameter so GO386=softfloat can be passed without duplicating build logic Tested on iPhone running iSH with Alpine Linux. The binary starts and successfully relays traffic with --max-common-clients 2. Usage on iSH: apk add curl curl -L -o conduit https://github.com/.../conduit-linux-386 chmod +x conduit ./conduit start --max-common-clients 2 --- .github/workflows/release-cli.yml | 3 +++ cli/Makefile | 16 ++++++++++++---- cli/cmd/ish_linux_386.go | 13 +++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 cli/cmd/ish_linux_386.go diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 71c378e3..9b150a1e 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -111,10 +111,12 @@ jobs: cli/dist/conduit-linux-arm64 cli/dist/conduit-linux-armv7 cli/dist/conduit-linux-armv6 + cli/dist/conduit-linux-386 cli/dist/conduit-monitor-linux-amd64 cli/dist/conduit-monitor-linux-arm64 cli/dist/conduit-monitor-linux-armv7 cli/dist/conduit-monitor-linux-armv6 + cli/dist/conduit-monitor-linux-386 cli/dist/checksums.txt - name: Create Release @@ -130,6 +132,7 @@ jobs: cli/dist/conduit-linux-arm64 cli/dist/conduit-linux-armv7 cli/dist/conduit-linux-armv6 + cli/dist/conduit-linux-386 cli/dist/conduit-darwin-amd64 cli/dist/conduit-darwin-arm64 cli/dist/conduit-windows-amd64.exe diff --git a/cli/Makefile b/cli/Makefile index 54168f23..0b2cc3cb 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -24,15 +24,16 @@ PSIPHON_REPO := https://github.com/Psiphon-Labs/psiphon-tunnel-core.git # $(3) = GOOS # $(4) = GOARCH # $(5) = GOARM (optional) +# $(6) = extra env vars (optional, e.g. GO386=softfloat) define GO_BUILD @BUILDDATE=$$(date +%Y-%m-%dT%H:%M:%S%z) && \ BUILDREPO=$$(cd psiphon-tunnel-core && git config --get remote.origin.url) && \ BUILDREV=$$(cd psiphon-tunnel-core && git rev-parse --short HEAD) && \ GOVERSION="$$($(GO) version | sed 's/go version //')" && \ ALL_TAGS="$(BASE_TAGS)" && \ - if [ -n "$(2)" ]; then ALL_TAGS="$$ALL_TAGS,$(2)"; fi && \ + if [ -n "$(2)" ]; then ALL_TAGS="$$ALL_TAGS $(2)"; fi && \ echo "Building $(3)/$(4) -> $(1) with tags: $$ALL_TAGS" && \ - CGO_ENABLED=0 GOOS=$(3) GOARCH=$(4) $(if $(5),GOARM=$(5)) $(GO) build \ + CGO_ENABLED=0 GOOS=$(3) GOARCH=$(4) $(if $(5),GOARM=$(5)) $(if $(6),$(6)) $(GO) build \ -tags "$$ALL_TAGS" \ -ldflags "\ -s -w \ @@ -50,9 +51,10 @@ endef # $(2) = GOOS # $(3) = GOARCH # $(4) = GOARM (optional) +# $(5) = extra env vars (optional, e.g. GO386=softfloat) define GO_BUILD_MONITOR @echo "Building monitor $(2)/$(3) -> $(1)" - @CGO_ENABLED=0 GOOS=$(2) GOARCH=$(3) $(if $(4),GOARM=$(4)) $(GO) build \ + @CGO_ENABLED=0 GOOS=$(2) GOARCH=$(3) $(if $(4),GOARM=$(4)) $(if $(5),$(5)) $(GO) build \ -ldflags "-s -w" \ -o $(1) ./scripts/monitor endef @@ -133,6 +135,10 @@ build-linux-armv6: check-go check-setup $(call GO_BUILD,dist/conduit-linux-armv6,,linux,arm,6) $(call GO_BUILD_MONITOR,dist/conduit-monitor-linux-armv6,linux,arm,6) +build-linux-386: check-go check-setup + $(call GO_BUILD,dist/conduit-linux-386,netgo,linux,386,,GO386=softfloat) + $(call GO_BUILD_MONITOR,dist/conduit-monitor-linux-386,linux,386,,GO386=softfloat) + build-darwin: check-go check-setup $(call GO_BUILD,dist/conduit-darwin-amd64,,darwin,amd64) $(call GO_BUILD_MONITOR,dist/conduit-monitor-darwin-amd64,darwin,amd64) @@ -150,7 +156,7 @@ build-freebsd: check-go check-setup $(call GO_BUILD_MONITOR,dist/conduit-monitor-freebsd-amd64,freebsd,amd64) # Build for all platforms -build-all: check-go check-setup build-linux build-linux-arm build-linux-armv7 build-linux-armv6 build-darwin build-darwin-arm build-windows build-freebsd +build-all: check-go check-setup build-linux build-linux-arm build-linux-armv7 build-linux-armv6 build-linux-386 build-darwin build-darwin-arm build-windows build-freebsd # Build for all platforms with embedded config build-all-embedded: check-go check-setup check-psiphon-config @@ -160,6 +166,7 @@ build-all-embedded: check-go check-setup check-psiphon-config $(call GO_BUILD,dist/conduit-linux-arm64,$(EMBED_TAG),linux,arm64) $(call GO_BUILD,dist/conduit-linux-armv7,$(EMBED_TAG),linux,arm,7) $(call GO_BUILD,dist/conduit-linux-armv6,$(EMBED_TAG),linux,arm,6) + $(call GO_BUILD,dist/conduit-linux-386,$(EMBED_TAG) netgo,linux,386,,GO386=softfloat) $(call GO_BUILD,dist/conduit-darwin-amd64,$(EMBED_TAG),darwin,amd64) $(call GO_BUILD,dist/conduit-darwin-arm64,$(EMBED_TAG),darwin,arm64) $(call GO_BUILD,dist/conduit-windows-amd64.exe,$(EMBED_TAG),windows,amd64) @@ -170,6 +177,7 @@ build-all-embedded: check-go check-setup check-psiphon-config $(call GO_BUILD_MONITOR,dist/conduit-monitor-linux-arm64,linux,arm64) $(call GO_BUILD_MONITOR,dist/conduit-monitor-linux-armv7,linux,arm,7) $(call GO_BUILD_MONITOR,dist/conduit-monitor-linux-armv6,linux,arm,6) + $(call GO_BUILD_MONITOR,dist/conduit-monitor-linux-386,linux,386,,GO386=softfloat) $(call GO_BUILD_MONITOR,dist/conduit-monitor-darwin-amd64,darwin,amd64) $(call GO_BUILD_MONITOR,dist/conduit-monitor-darwin-arm64,darwin,arm64) $(call GO_BUILD_MONITOR,dist/conduit-monitor-windows-amd64.exe,windows,amd64) diff --git a/cli/cmd/ish_linux_386.go b/cli/cmd/ish_linux_386.go new file mode 100644 index 00000000..925c0094 --- /dev/null +++ b/cli/cmd/ish_linux_386.go @@ -0,0 +1,13 @@ +//go:build linux && 386 + +package cmd + +import "runtime" + +func init() { + // iSH's %gs TLS emulation is unreliable in signal handler context. With + // multiple Ps, Go sends SIGURG to preempt goroutines across OS threads, + // which triggers a "bad g in signal handler" crash. GOMAXPROCS=1 keeps + // all goroutines on a single P, preventing cross-thread SIGURG delivery. + runtime.GOMAXPROCS(1) +} From 5604a3c01d4bd7d4f572803c228768de554705dd Mon Sep 17 00:00:00 2001 From: badoriie Date: Wed, 27 May 2026 21:36:28 +0200 Subject: [PATCH 2/2] Fix concurrent session refresh race condition Multiple background queries (useHostedAccountProfileQuery, useHostedConduitsQuery) each call withHostedSessionRecovery on mount. When the loaded session is expiring, all of them independently call sessionClient.refresh() before any one of them has written the refreshed token back to the query cache. This results in redundant parallel refresh requests. Fix by coalescing concurrent refresh calls for the same queryClient+baseUrl into a single in-flight promise, using a WeakMap> so all concurrent callers share one network request. WeakMap keying on queryClient ensures each test gets an isolated map without any teardown needed. --- src/hosted/sessionQueries.ts | 47 ++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/hosted/sessionQueries.ts b/src/hosted/sessionQueries.ts index d12dec81..e5f277a9 100644 --- a/src/hosted/sessionQueries.ts +++ b/src/hosted/sessionQueries.ts @@ -92,21 +92,48 @@ export async function ensureHostedSession( return refreshHostedSession(queryClient, input); } +// Deduplicates concurrent refresh calls for the same queryClient+baseUrl so +// that parallel queries racing on an expiring token only trigger one network +// request. WeakMap keying avoids cross-test pollution. +const inflightRefreshes = new WeakMap< + QueryClient, + Map> +>(); + export async function refreshHostedSession( queryClient: QueryClient, input: HostedSessionDependencies, ): Promise { - try { - const refreshed = await input.sessionClient.refresh(); - await setHostedSessionState(queryClient, input, refreshed); - return refreshed; - } catch (error) { - if (error instanceof HostedApiRequestError && error.status === 401) { - await clearHostedSessionState(queryClient, input); - throw new Error("Hosted session expired; please sign in again"); - } - throw error; + let byUrl = inflightRefreshes.get(queryClient); + if (!byUrl) { + byUrl = new Map(); + inflightRefreshes.set(queryClient, byUrl); } + + const existing = byUrl.get(input.baseUrl); + if (existing) return existing; + + const promise = (async () => { + try { + const refreshed = await input.sessionClient.refresh(); + await setHostedSessionState(queryClient, input, refreshed); + return refreshed; + } catch (error) { + if ( + error instanceof HostedApiRequestError && + error.status === 401 + ) { + await clearHostedSessionState(queryClient, input); + throw new Error("Hosted session expired; please sign in again"); + } + throw error; + } finally { + byUrl.delete(input.baseUrl); + } + })(); + + byUrl.set(input.baseUrl, promise); + return promise; } export async function withHostedSessionRecovery(