4 Commits
Author SHA1 Message Date
xweiba 27af50375d chore: prepare v1.0.4 release 2026-08-09 18:36:02 +08:00
xweiba a2f46dd166 docs: refine acknowledgements and links 2026-08-09 17:59:21 +08:00
xweiba b0d2f0e8e4 feat: improve wloc response compatibility 2026-08-09 16:23:41 +08:00
xweiba 7cf38363cf docs: add community acknowledgements 2026-08-09 16:00:37 +08:00
12 changed files with 288 additions and 18 deletions
+102
View File
@@ -41,6 +41,8 @@ type wireField struct {
var macPattern = regexp.MustCompile(`^[0-9a-fA-F]{1,2}(:[0-9a-fA-F]{1,2}){5}$`)
var wlocMarker = []byte{0, 0, 0, 1, 0, 0}
func minInt(a, b int) int {
if a < b {
return a
@@ -314,6 +316,99 @@ func patchWlocPayload(payload []byte, c wlocCoords, st *patchStats) ([]byte, boo
return out, changed, nil
}
func parseARPCPayloadBounds(body []byte) (lengthOffset, payloadOffset, payloadEnd int, err error) {
if len(body) < 2 {
return 0, 0, 0, errors.New("ARPC body too short")
}
offset := 2 // version
for range 3 {
if offset+2 > len(body) {
return 0, 0, 0, errors.New("truncated ARPC string length")
}
length := int(binary.BigEndian.Uint16(body[offset : offset+2]))
offset += 2
if length > len(body)-offset {
return 0, 0, 0, errors.New("truncated ARPC string")
}
offset += length
}
const functionAndLengthBytes = 8
if offset+functionAndLengthBytes > len(body) {
return 0, 0, 0, errors.New("truncated ARPC header")
}
lengthOffset = offset + 4
payloadOffset = lengthOffset + 4
payloadLength := uint64(binary.BigEndian.Uint32(body[lengthOffset:payloadOffset]))
if payloadLength == 0 || payloadLength > uint64(len(body)-payloadOffset) {
return 0, 0, 0, errors.New("invalid ARPC payload length")
}
return lengthOffset, payloadOffset, payloadOffset + int(payloadLength), nil
}
func patchARPCFrame(body []byte, c wlocCoords) ([]byte, patchStats, error) {
lengthOffset, payloadOffset, payloadEnd, err := parseARPCPayloadBounds(body)
if err != nil {
return nil, patchStats{}, err
}
var st patchStats
payload := body[payloadOffset:payloadEnd]
newPayload, changed, err := patchWlocPayload(payload, c, &st)
if err != nil {
return nil, patchStats{}, err
}
if !changed || bytes.Equal(newPayload, payload) {
return nil, patchStats{}, errors.New("ARPC envelope has no patchable wloc payload")
}
var lenBytes [4]byte
binary.BigEndian.PutUint32(lenBytes[:], uint32(len(newPayload)))
out := append(cloneBytes(body[:lengthOffset]), lenBytes[:]...)
out = append(out, newPayload...)
out = append(out, body[payloadEnd:]...)
return out, st, nil
}
func patchMarkerFrame(body []byte, c wlocCoords) ([]byte, patchStats, error) {
markerOffset := bytes.Index(body, wlocMarker)
if markerOffset < 0 {
return nil, patchStats{}, errors.New("wloc marker not found")
}
lengthOffset := markerOffset + len(wlocMarker)
payloadOffset := lengthOffset + 2
if payloadOffset > len(body) {
return nil, patchStats{}, errors.New("truncated marker frame")
}
payloadLength := int(binary.BigEndian.Uint16(body[lengthOffset:payloadOffset]))
if payloadLength == 0 || payloadLength > len(body)-payloadOffset {
return nil, patchStats{}, errors.New("invalid marker payload length")
}
payloadEnd := payloadOffset + payloadLength
var st patchStats
payload := body[payloadOffset:payloadEnd]
newPayload, changed, err := patchWlocPayload(payload, c, &st)
if err != nil {
return nil, patchStats{}, err
}
if !changed || bytes.Equal(newPayload, payload) {
return nil, patchStats{}, errors.New("marker frame has no patchable wloc payload")
}
if len(newPayload) > 65535 {
return nil, patchStats{}, errors.New("patched marker payload too large")
}
var lenBytes [2]byte
binary.BigEndian.PutUint16(lenBytes[:], uint16(len(newPayload)))
out := append(cloneBytes(body[:lengthOffset]), lenBytes[:]...)
out = append(out, newPayload...)
out = append(out, body[payloadEnd:]...)
return out, st, nil
}
func patchFrame(body []byte, offset int, c wlocCoords, st *patchStats) ([]byte, patchStats, error) {
if len(body) < offset+10 {
return nil, *st, fmt.Errorf("body too short: %d, base=%d", len(body), offset)
@@ -352,6 +447,13 @@ func patchFrame(body []byte, offset int, c wlocCoords, st *patchStats) ([]byte,
}
func patchWlocBody(body []byte, c wlocCoords) ([]byte, patchStats, error) {
if out, st, err := patchARPCFrame(body, c); err == nil {
return out, st, nil
}
if out, st, err := patchMarkerFrame(body, c); err == nil {
return out, st, nil
}
var st patchStats
offsets := []int{0, 2, 4, 6, 8, 10, 12, 14, 16}
seen := map[int]bool{}
+131 -7
View File
@@ -44,6 +44,29 @@ func testFrame(payload []byte) []byte {
return out
}
func testARPCFrame(payload, suffix []byte) ([]byte, int) {
var out []byte
var version [2]byte
binary.BigEndian.PutUint16(version[:], 1)
out = append(out, version[:]...)
for _, value := range [][]byte{[]byte("zh_CN"), []byte("com.apple.locationd"), []byte("20A123")} {
var length [2]byte
binary.BigEndian.PutUint16(length[:], uint16(len(value)))
out = append(out, length[:]...)
out = append(out, value...)
}
var functionID [4]byte
binary.BigEndian.PutUint32(functionID[:], 1)
out = append(out, functionID[:]...)
lengthOffset := len(out)
var payloadLength [4]byte
binary.BigEndian.PutUint32(payloadLength[:], uint32(len(payload)))
out = append(out, payloadLength[:]...)
out = append(out, payload...)
out = append(out, suffix...)
return out, lengthOffset
}
func TestPatchWifiLocation(t *testing.T) {
payload := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
body := testFrame(payload)
@@ -69,20 +92,121 @@ func TestPatchWifiLocation(t *testing.T) {
}
func TestPatchCellLocation(t *testing.T) {
cell := writeLengthDelimited(5, testLocation(300, 400, 25))
payload := writeLengthDelimited(22, cell)
body := testFrame(payload)
c := wlocCoords{Latitude: 22.544577, Longitude: 113.94114, Accuracy: 25}
for _, field := range []int{22, 24} {
t.Run(fmt.Sprintf("field_%d", field), func(t *testing.T) {
cell := writeLengthDelimited(5, testLocation(300, 400, 25))
payload := writeLengthDelimited(field, cell)
body := testFrame(payload)
c := wlocCoords{Latitude: 22.544577, Longitude: 113.94114, Accuracy: 25}
patched, stats, err := patchWlocBody(body, c)
if err != nil {
t.Fatal(err)
}
if stats.Cell != 1 || stats.Locations != 1 {
t.Fatalf("unexpected stats: %+v", stats)
}
if bytes.Equal(patched, body) {
t.Fatal("body was not patched")
}
})
}
}
func TestPatchARPCFramePreservesEnvelopeAndSuffix(t *testing.T) {
payload := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
suffix := []byte{0xde, 0xad, 0xbe, 0xef}
body, lengthOffset := testARPCFrame(payload, suffix)
originalPrefix := cloneBytes(body[:lengthOffset])
c := wlocCoords{Latitude: 31.230416, Longitude: 121.473701, Accuracy: 50}
patched, stats, err := patchWlocBody(body, c)
if err != nil {
t.Fatal(err)
}
if stats.Cell != 1 || stats.Locations != 1 {
if stats.WiFi != 1 || stats.Locations != 1 {
t.Fatalf("unexpected stats: %+v", stats)
}
if bytes.Equal(patched, body) {
t.Fatal("body was not patched")
if !bytes.Equal(patched[:lengthOffset], originalPrefix) {
t.Fatal("ARPC metadata changed")
}
newLength := int(binary.BigEndian.Uint32(patched[lengthOffset : lengthOffset+4]))
if newLength == len(payload) {
t.Fatal("ARPC payload length was not updated")
}
if !bytes.Equal(patched[lengthOffset+4+newLength:], suffix) {
t.Fatal("ARPC suffix changed")
}
}
func TestPatchARPCPayloadLargerThanUint16(t *testing.T) {
padding := writeLengthDelimited(99, bytes.Repeat([]byte{0x7f}, 70_000))
location := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
payload := append(padding, location...)
body, lengthOffset := testARPCFrame(payload, nil)
c := wlocCoords{Latitude: 31.230416, Longitude: 121.473701, Accuracy: 50}
patched, stats, err := patchWlocBody(body, c)
if err != nil {
t.Fatal(err)
}
if stats.WiFi != 1 || stats.Locations != 1 {
t.Fatalf("unexpected stats: %+v", stats)
}
newLength := int(binary.BigEndian.Uint32(patched[lengthOffset : lengthOffset+4]))
if newLength <= 65535 {
t.Fatalf("expected 32-bit ARPC payload length, got %d", newLength)
}
if !bytes.Contains(patched[lengthOffset+4:lengthOffset+4+newLength], padding) {
t.Fatal("unknown ARPC payload field changed")
}
}
func TestPatchMarkerFramePreservesPrefixAndSuffix(t *testing.T) {
payload := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
prefix := []byte{0xaa, 0xbb, 0xcc}
suffix := []byte{0xdd, 0xee}
var length [2]byte
binary.BigEndian.PutUint16(length[:], uint16(len(payload)))
body := append(cloneBytes(prefix), wlocMarker...)
body = append(body, length[:]...)
body = append(body, payload...)
body = append(body, suffix...)
c := wlocCoords{Latitude: 31.230416, Longitude: 121.473701, Accuracy: 50}
patched, stats, err := patchWlocBody(body, c)
if err != nil {
t.Fatal(err)
}
if stats.WiFi != 1 || stats.Locations != 1 {
t.Fatalf("unexpected stats: %+v", stats)
}
lengthOffset := len(prefix) + len(wlocMarker)
newLength := int(binary.BigEndian.Uint16(patched[lengthOffset : lengthOffset+2]))
if newLength == len(payload) {
t.Fatal("marker payload length was not updated")
}
if !bytes.Equal(patched[:len(prefix)], prefix) {
t.Fatal("marker prefix changed")
}
if !bytes.Equal(patched[lengthOffset+2+newLength:], suffix) {
t.Fatal("marker suffix changed")
}
}
func TestPatchBareWlocPayload(t *testing.T) {
body := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
c := wlocCoords{Latitude: 31.230416, Longitude: 121.473701, Accuracy: 50}
patched, stats, err := patchWlocBody(body, c)
if err != nil {
t.Fatal(err)
}
if stats.WiFi != 1 || stats.Locations != 1 {
t.Fatalf("unexpected stats: %+v", stats)
}
if len(patched) > 0 && bytes.HasPrefix(patched, []byte{0, 1, 0, 0}) {
t.Fatal("bare payload was unexpectedly wrapped")
}
}
+9 -2
View File
@@ -12,7 +12,7 @@ responses in a controlled test environment.
[![iOS 15+](https://img.shields.io/badge/iOS-15%2B-111111?logo=apple)](project.yml)
[![Swift 5.9](https://img.shields.io/badge/Swift-5.9-F05138)](project.yml)
[![Go 1.23+](https://img.shields.io/badge/Go-1.23%2B-00ADD8?logo=go)](Core/go.mod)
[![Version](https://img.shields.io/badge/version-v1.0.3-2563EB)](docs/CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-v1.0.4-2563EB)](docs/CHANGELOG.md)
[Features](#feature-overview) ·
[How It Works](#how-it-works) ·
@@ -443,10 +443,17 @@ guarantee for every app or release.
The core location-response handling approach, Go implementation, and third-party modules are based on:
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
Community link:
Thanks to the following LINUX DO users for their contributions:
- Bug fixes: [Chen Ze](https://linux.do/u/lixiaobaivv)
- Ideas and suggestions: [Alex](https://linux.do/u/_alex), [ye4241](https://linux.do/u/ye4241)
Links:
- [LINUX DO](https://linux.do/)
- [iOS-Location-Spoofer-Web](https://github.com/akudamatata/iOS-Location-Spoofer-Web)
Thanks to the open-source contributors working on iOS location-service research, network proxies, and mobile testing
tools.
+8 -1
View File
@@ -12,7 +12,7 @@
[![iOS 15+](https://img.shields.io/badge/iOS-15%2B-111111?logo=apple)](project.yml)
[![Swift 5.9](https://img.shields.io/badge/Swift-5.9-F05138)](project.yml)
[![Go 1.23+](https://img.shields.io/badge/Go-1.23%2B-00ADD8?logo=go)](Core/go.mod)
[![Version](https://img.shields.io/badge/version-v1.0.3-2563EB)](docs/CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-v1.0.4-2563EB)](docs/CHANGELOG.md)
[功能概览](#功能概览) ·
[工作原理](#工作原理) ·
@@ -431,9 +431,16 @@ GitHub Issue Form 中的“App 生成的诊断报告”字段与 App 复制内
核心定位响应处理思路、Go 实现和第三方模块参考自:
- [Yu9191/wloc](https://github.com/Yu9191/wloc)
- [ios-location-spoofer](https://github.com/mekos2772/ios-location-spoofer)
感谢以下 LINUX DO 用户对项目的贡献:
- 功能修复:[陈泽](https://linux.do/u/lixiaobaivv)
- 思路及建议:[Alex](https://linux.do/u/_alex)、[ye4241](https://linux.do/u/ye4241)
友链:
- [LINUX DO](https://linux.do/)
- [iOS-Location-Spoofer-Web](https://github.com/akudamatata/iOS-Location-Spoofer-Web)
感谢开源社区中参与 iOS 定位服务研究、网络代理和移动端测试工具建设的贡献者。
+1 -1
View File
@@ -23,7 +23,7 @@ struct AppRemoteConfiguration: Decodable, Equatable {
let communityPromptClients: [String]
static let fallback = AppRemoteConfiguration(
latestVersion: "1.0.3",
latestVersion: "1.0.4",
minimumSupportedVersion: "1.0.0",
communityPromptClients: [
ThirdPartyProxyClient.surge.rawValue,
@@ -62,7 +62,7 @@ final class AppRemoteConfigurationTests: XCTestCase {
func testFallbackMatchesCurrentProjectPolicy() {
let configuration = AppRemoteConfiguration.fallback
XCTAssertEqual(configuration.latestVersion, "1.0.3")
XCTAssertEqual(configuration.latestVersion, "1.0.4")
XCTAssertEqual(configuration.minimumSupportedVersion, "1.0.0")
XCTAssertFalse(configuration.requestsCommunityPrompt(for: .shadowrocket))
for client in ThirdPartyProxyClient.allCases where client != .shadowrocket {
+2 -2
View File
@@ -112,7 +112,7 @@ test -s "$ROOT/docs/onboarding-screenshots/shadowrocket/shadowrocket-module-impo
grep -q 'presentSuccessfulOperationTip(.activation)' "$ROOT/App/MapHomeView.swift" || fail "third-party save must present the activation tip"
grep -q 'presentSuccessfulOperationTip(.deactivation)' "$ROOT/App/MapHomeView.swift" || fail "third-party clear must present the deactivation tip"
grep -q 'if spoofState == .active' "$ROOT/App/MapHomeView.swift" || fail "manual help must follow the shared spoof state"
grep -q 'MARKETING_VERSION: "1.0.3"' "$ROOT/project.yml" || fail "marketing version must be 1.0.3"
grep -q 'CURRENT_PROJECT_VERSION: "4"' "$ROOT/project.yml" || fail "build version must be 4"
grep -q 'MARKETING_VERSION: "1.0.4"' "$ROOT/project.yml" || fail "marketing version must be 1.0.4"
grep -q 'CURRENT_PROJECT_VERSION: "5"' "$ROOT/project.yml" || fail "build version must be 5"
echo "PASS: third-party proxy mode contract"
@@ -25,7 +25,7 @@ import sys
with open(sys.argv[1], encoding="utf-8") as handle:
config = json.load(handle)
assert config["latestVersion"] == "1.0.3"
assert config["latestVersion"] == "1.0.4"
assert config["minimumSupportedVersion"] == "1.0.0"
assert "shadowrocket" not in config["communityPromptClients"]
assert set(config["communityPromptClients"]) == {
+1
View File
@@ -4,6 +4,7 @@
## 已发布
- [v1.0.4](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.4) — 2026-08-09
- [v1.0.3](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.3) — 2026-08-09
- [v1.0.2](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.2) — 2026-08-07
- [v1.0.1](https://github.com/xweiba/location-spoofer/releases/tag/v1.0.1) — 2026-08-06
+29
View File
@@ -0,0 +1,29 @@
# v1.0.4
发布日期:2026-08-09
## 主要更新
- 增加结构化 ARPC 响应解析,修改定位数据后会正确更新 32 位 payload 长度。
- 完善 marker、synthetic 和 bare protobuf 响应兼容,保留原始封装前后缀及未知字段。
- 补充 CellTower 字段 22/24、ARPC 大 payload 和多响应格式回归测试。
## 文档与致谢
- 整理项目参考、社区贡献和友链结构。
- 补充相关定位服务研究项目致谢。
## 兼容性说明
- WiFi 和 CellTower 坐标替换逻辑保持不变。
- 无法识别的新响应封装会回退原有检测路径,不会截断响应。
- 本版本未写入语义尚未确认的运动状态字段。
## 自签安装
- Release 附件为未签名 IPA,安装前需要自行签名。
- 可使用免费 Apple ID 和 Impactor 完成签名安装,无需付费开发者账号。
- 签名时请保留 Bundle ID `com.paopaolabs.location-spoofer`、App Group `group.com.paopaolabs.location-spoofer` 及原有 entitlements。
- 免费 Apple ID 签名通常只有 7 天有效期,到期后需要重新签名安装。
<!-- commit-range: v1.0.3..v1.0.4 -->
+2 -2
View File
@@ -8,8 +8,8 @@ options:
settings:
base:
SWIFT_VERSION: "5.9"
MARKETING_VERSION: "1.0.3"
CURRENT_PROJECT_VERSION: "4"
MARKETING_VERSION: "1.0.4"
CURRENT_PROJECT_VERSION: "5"
CODE_SIGN_STYLE: Manual
CODE_SIGNING_ALLOWED: "NO"
CODE_SIGNING_REQUIRED: "NO"
+1 -1
View File
@@ -1,5 +1,5 @@
{
"latestVersion": "1.0.3",
"latestVersion": "1.0.4",
"minimumSupportedVersion": "1.0.0",
"communityPromptClients": ["surge", "quantumultX", "loon", "stash", "egern"]
}