diff --git a/client/ish_listener_linux_386.go b/client/ish_listener_linux_386.go new file mode 100644 index 0000000..b2fe729 --- /dev/null +++ b/client/ish_listener_linux_386.go @@ -0,0 +1,74 @@ +//go:build linux && 386 +package main + +import ( + "net" + "os" + "syscall" + "time" + "unsafe" +) + +type ishListener struct { + net.Listener + fd int +} + +// wrapISHListener overrides the standard net.Listener with a legacy syscall listener +// designed specifically for the iSH simulator on iOS, which lacks modern `accept4`. +func wrapISHListener(ln net.Listener) (net.Listener, error) { + tl, ok := ln.(*net.TCPListener) + if !ok { + return ln, nil + } + f, err := tl.File() + if err != nil { + return nil, err + } + + return &ishListener{Listener: ln, fd: int(f.Fd())}, nil +} + +func (l *ishListener) Accept() (net.Conn, error) { + for { + addr := make([]byte, 128) + addrlen := uintptr(128) + + // i386 network syscalls are multiplexed via socketcall (102). + // SYS_ACCEPT is subcall 5. + args := [3]uintptr{uintptr(l.fd), uintptr(unsafe.Pointer(&addr[0])), uintptr(unsafe.Pointer(&addrlen))} + + r1, _, errno := syscall.Syscall(102, 5, uintptr(unsafe.Pointer(&args)), 0) + if errno != 0 { + if errno == syscall.EAGAIN || errno == syscall.EINTR || errno == syscall.EWOULDBLOCK { + time.Sleep(50 * time.Millisecond) // Just in case it's non-blocking somehow + continue + } + return nil, errno + } + + nfd := int(r1) + + // Wrap raw FD into os.File, then into a net.Conn. + // fileConn duplicates the fd again. + f := os.NewFile(uintptr(nfd), "ish-conn") + conn, err := net.FileConn(f) + f.Close() + + if err != nil { + syscall.Close(nfd) + return nil, err + } + + return conn, nil + } +} + +func (l *ishListener) Close() error { + err1 := syscall.Close(l.fd) + err2 := l.Listener.Close() + if err1 != nil { + return err1 + } + return err2 +} diff --git a/client/ish_listener_other.go b/client/ish_listener_other.go new file mode 100644 index 0000000..a100a1b --- /dev/null +++ b/client/ish_listener_other.go @@ -0,0 +1,9 @@ +//go:build !(linux && 386) +package main + +import "net" + +// wrapISHListener is a no-op for architectures that don't need the legacy socketcall accept bypass. +func wrapISHListener(ln net.Listener) (net.Listener, error) { + return ln, nil +} diff --git a/client/main.go b/client/main.go index 6186914..9766723 100644 --- a/client/main.go +++ b/client/main.go @@ -2036,12 +2036,19 @@ func runVLESSMode(ctx context.Context, tp *turnParams, peer *net.UDPAddr, listen if err != nil { log.Panicf("TCP listen: %s", err) } - context.AfterFunc(ctx, func() { _ = listener.Close() }) + + wrappedListener, err := wrapISHListener(listener) + if err != nil { + log.Printf("Warning: failed to wrap listener: %v", err) + wrappedListener = listener + } + + context.AfterFunc(ctx, func() { _ = wrappedListener.Close() }) log.Printf("VLESS mode: listening on %s (round-robin across %d sessions)", listenAddr, numSessions) var wgConn sync.WaitGroup for { - tcpConn, err := listener.Accept() + tcpConn, err := wrappedListener.Accept() if err != nil { select { case <-ctx.Done(): diff --git a/client/slider_captcha.go b/client/slider_captcha.go index 166a6ab..fb0cc96 100644 --- a/client/slider_captcha.go +++ b/client/slider_captcha.go @@ -715,12 +715,70 @@ func rankSliderCandidates(img image.Image, gridSize int, swaps []int) ([]sliderC } func scoreSliderCandidate(img image.Image, gridSize int, mapping []int) (int64, error) { - rendered, err := renderSliderCandidate(img, gridSize, mapping) - if err != nil { - return 0, err + bounds := img.Bounds() + var score int64 + + for row := 0; row < gridSize; row++ { + for col := 0; col < gridSize-1; col++ { + dstLeftIndex := row*gridSize + col + dstRightIndex := row*gridSize + col + 1 + srcLeftIndex := mapping[dstLeftIndex] + srcRightIndex := mapping[dstRightIndex] + + dstLeftRect := sliderTileRect(bounds, gridSize, dstLeftIndex) + dstRightRect := sliderTileRect(bounds, gridSize, dstRightIndex) + srcLeftRect := sliderTileRect(bounds, gridSize, srcLeftIndex) + srcRightRect := sliderTileRect(bounds, gridSize, srcRightIndex) + + height := minInt(dstLeftRect.Dy(), dstRightRect.Dy()) + + leftSrcXRel := (dstLeftRect.Dx() - 1) * srcLeftRect.Dx() / dstLeftRect.Dx() + sxLeft := srcLeftRect.Min.X + leftSrcXRel + sxRight := srcRightRect.Min.X + + for offset := 0; offset < height; offset++ { + syLeft := srcLeftRect.Min.Y + offset*srcLeftRect.Dy()/dstLeftRect.Dy() + syRight := srcRightRect.Min.Y + offset*srcRightRect.Dy()/dstRightRect.Dy() + + score += pixelDiff( + img.At(sxLeft, syLeft), + img.At(sxRight, syRight), + ) + } + } + } + + for row := 0; row < gridSize-1; row++ { + for col := 0; col < gridSize; col++ { + dstTopIndex := row*gridSize + col + dstBottomIndex := (row+1)*gridSize + col + srcTopIndex := mapping[dstTopIndex] + srcBottomIndex := mapping[dstBottomIndex] + + dstTopRect := sliderTileRect(bounds, gridSize, dstTopIndex) + dstBottomRect := sliderTileRect(bounds, gridSize, dstBottomIndex) + srcTopRect := sliderTileRect(bounds, gridSize, srcTopIndex) + srcBottomRect := sliderTileRect(bounds, gridSize, srcBottomIndex) + + width := minInt(dstTopRect.Dx(), dstBottomRect.Dx()) + + topSrcYRel := (dstTopRect.Dy() - 1) * srcTopRect.Dy() / dstTopRect.Dy() + syTop := srcTopRect.Min.Y + topSrcYRel + syBottom := srcBottomRect.Min.Y + + for offset := 0; offset < width; offset++ { + sxTop := srcTopRect.Min.X + offset*srcTopRect.Dx()/dstTopRect.Dx() + sxBottom := srcBottomRect.Min.X + offset*srcBottomRect.Dx()/dstBottomRect.Dx() + + score += pixelDiff( + img.At(sxTop, syTop), + img.At(sxBottom, syBottom), + ) + } + } } - return scoreRenderedSliderImage(rendered, gridSize), nil + return score, nil } func renderSliderCandidate(img image.Image, gridSize int, mapping []int) (*image.RGBA, error) { @@ -744,40 +802,6 @@ func renderSliderCandidate(img image.Image, gridSize int, mapping []int) (*image return rendered, nil } -func scoreRenderedSliderImage(img image.Image, gridSize int) int64 { - bounds := img.Bounds() - var score int64 - - for row := 0; row < gridSize; row++ { - for col := 0; col < gridSize-1; col++ { - leftRect := sliderTileRect(bounds, gridSize, row*gridSize+col) - rightRect := sliderTileRect(bounds, gridSize, row*gridSize+col+1) - height := minInt(leftRect.Dy(), rightRect.Dy()) - for offset := 0; offset < height; offset++ { - score += pixelDiff( - img.At(leftRect.Max.X-1, leftRect.Min.Y+offset), - img.At(rightRect.Min.X, rightRect.Min.Y+offset), - ) - } - } - } - - for row := 0; row < gridSize-1; row++ { - for col := 0; col < gridSize; col++ { - topRect := sliderTileRect(bounds, gridSize, row*gridSize+col) - bottomRect := sliderTileRect(bounds, gridSize, (row+1)*gridSize+col) - width := minInt(topRect.Dx(), bottomRect.Dx()) - for offset := 0; offset < width; offset++ { - score += pixelDiff( - img.At(topRect.Min.X+offset, topRect.Max.Y-1), - img.At(bottomRect.Min.X+offset, bottomRect.Min.Y), - ) - } - } - } - - return score -} func sliderTileRect(bounds image.Rectangle, gridSize int, index int) image.Rectangle { row := index / gridSize