mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-22 14:50:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27af50375d | ||
|
|
a2f46dd166 | ||
|
|
b0d2f0e8e4 | ||
|
|
7cf38363cf |
@@ -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
@@ -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
@@ -12,7 +12,7 @@ responses in a controlled test environment.
|
||||
[](project.yml)
|
||||
[](project.yml)
|
||||
[](Core/go.mod)
|
||||
[](docs/CHANGELOG.md)
|
||||
[](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.
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
[](project.yml)
|
||||
[](project.yml)
|
||||
[](Core/go.mod)
|
||||
[](docs/CHANGELOG.md)
|
||||
[](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 定位服务研究、网络代理和移动端测试工具建设的贡献者。
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"]) == {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"latestVersion": "1.0.3",
|
||||
"latestVersion": "1.0.4",
|
||||
"minimumSupportedVersion": "1.0.0",
|
||||
"communityPromptClients": ["surge", "quantumultX", "loon", "stash", "egern"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user