Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Build Anywhere Unsigned IPA

on:
workflow_dispatch:
push:
branches: [main]
paths:
- '.github/workflows/build.yml'
- '.github/ipa-build-trigger'

permissions:
contents: read

jobs:
build:
name: macos-14 unsigned iphoneos build
runs-on: macos-15

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Select Xcode 26.3 toolchain
run: sudo xcode-select -s /Applications/Xcode_26.3.app

- name: Verify Xcode toolchain
run: |
xcode-select -p
xcodebuild -version
xcodebuild -showsdks | grep iphoneos || true

- name: Build Anywhere (Debug, iphoneos, unsigned)
run: |
set -euo pipefail
DERIVED_DATA="${RUNNER_TEMP}/DerivedData"
xcodebuild \
-project Anywhere.xcodeproj \
-scheme Anywhere \
-configuration Debug \
-sdk iphoneos \
-derivedDataPath "${DERIVED_DATA}" \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGN_IDENTITY="" \
DEVELOPMENT_TEAM="" \
ONLY_ACTIVE_ARCH=NO \
build

- name: Package unsigned IPA
run: |
set -euo pipefail
DERIVED_DATA="${RUNNER_TEMP}/DerivedData"
APP_PATH="$(find "${DERIVED_DATA}" -path '*/Build/Products/Debug-iphoneos/Anywhere.app' -type d | head -1)"

if [[ -z "${APP_PATH}" || ! -d "${APP_PATH}" ]]; then
echo "Anywhere.app not found under ${DERIVED_DATA}" >&2
find "${DERIVED_DATA}" -name 'Anywhere.app' -type d || true
exit 1
fi

rm -rf Payload Anywhere.ipa
mkdir -p Payload
cp -R "${APP_PATH}" Payload/
zip -qr Anywhere.ipa Payload
ls -lh Anywhere.ipa

- name: Upload unsigned IPA artifact
uses: actions/upload-artifact@v4
with:
name: Anywhere-Unsigned-IPA
path: Anywhere.ipa
if-no-files-found: error
118 changes: 118 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# AGENTS.md

Guidance for AI agents working in the Anywhere repository.

## Product overview

**Anywhere** is a native iOS/iPadOS/tvOS proxy/VPN client (Swift + vendored C). It is **not** a web app or server-side project. There is no `package.json`, Docker stack, or backend API to run locally.

| Target | Scheme | Purpose |
| --- | --- | --- |
| **Anywhere** (required) | `Anywhere` | Main SwiftUI app |
| **Network Extension** (required for VPN E2E) | `Anywhere Network Extension` | Packet tunnel (`NEPacketTunnelProvider`) |
| Anywhere TV (optional) | `Anywhere TV` | tvOS variant |
| Anywhere Widget (optional) | `Anywhere Widget` | WidgetKit VPN toggle |

## Development environment (macOS — required for build/run)

Full development requires **macOS with Xcode** (project `LastUpgradeVersion = 2640`, iOS deployment target **17.0+**).

1. Open `Anywhere.xcodeproj` in Xcode.
2. Select the **Anywhere** scheme and an iOS Simulator or device.
3. Set your own **Development Team** (project currently references team `C7AS5D38Q8`).
4. Build and run (`⌘R`). The Network Extension is embedded and launches when VPN connects.

**Signing:** Network Extension, App Group (`group.com.argsment.Anywhere`), and Keychain entitlements require valid Apple provisioning.

**SPM dependencies** (resolved by Xcode): [Argsment/BLAKE3](https://github.com/Argsment/BLAKE3.git), [Argsment/YAML](https://github.com/Argsment/YAML.git).

**Bundled data:** `Shared/DataStore/Rules.db` (SQLite routing rules, ~39k rules).

**No automated test targets** (`XCTest` / test plans) are configured in this repo.

### Useful commands (macOS only)

```bash
# Build for iOS Simulator
xcodebuild -project Anywhere.xcodeproj -scheme Anywhere \
-destination 'platform=iOS Simulator,name=iPhone 16' build

# Build Network Extension
xcodebuild -project Anywhere.xcodeproj -scheme "Anywhere Network Extension" \
-destination 'platform=iOS Simulator,name=iPhone 16' build
```

### External runtime dependencies (for meaningful E2E proxy testing)

- User-configured proxy servers (VLESS, Hysteria2, Trojan, Shadowsocks, etc.) or subscription URLs
- Not part of the repo; needed to test real proxy connectivity beyond UI/build

## Code navigation

| Area | Path |
| --- | --- |
| Proxy protocols | `Shared/Networking/Protocols/` |
| Packet tunnel / lwIP / MITM | `Anywhere Network Extension/` |
| Shared models & stores | `Shared/` |
| Routing rules docs | `Documentations/Routing.md` |
| MITM rewrite docs | `Documentations/MITM.md` |

**Important:** `README.md` is a curated summary, not a spec. Verify behavior in source code when making claims about protocol support.

## Cursor Cloud specific instructions

### Platform limitation

This Cloud Agent VM runs **Linux**. The Anywhere iOS app **cannot be built or run here** — there is no Xcode, iOS SDK, or Simulator. Treat macOS + Xcode as a hard requirement for compile/run/debug workflows.

### What works on Linux (validation only)

Agents can still verify repo health without macOS:

1. **Project structure** — `Anywhere.xcodeproj`, four shared schemes, `Rules.db` present.
2. **SPM remotes** — pins in `Package.resolved` match `main` on GitHub (`Argsment/BLAKE3`, `Argsment/YAML`).
3. **Rules.db** — `sqlite3 Shared/DataStore/Rules.db ".tables"` → `metadata`, `rules`.
4. **SPM package compile** — BLAKE3 and YAML build with Swift on Linux (`swift build` in cloned repos); this validates dependency availability, not the iOS app.

Swift toolchain (if installed on the VM): `/opt/swift/usr/bin` — add to `PATH` before running `swift`.

### What does NOT work on Linux

- `xcodebuild`, iOS Simulator, device deploy, VPN/Network Extension testing
- SwiftUI preview, WidgetKit, JavaScriptCore MITM scripting in-app
- Lint/format — no `.swiftlint.yml` or CI lint config in repo

### Services

| Service | Linux VM | macOS + Xcode |
| --- | --- | --- |
| Anywhere app | Cannot run | Required |
| Network Extension | Cannot run | Required for VPN E2E |
| External proxy server | N/A (user-provided) | Optional for real traffic tests |

No long-running dev servers, databases, or Docker compose stacks exist in this repository.

### Headless cloud compilation (GitHub Actions)

Linux agents can orchestrate **unsigned IPA builds** on `macos-14` runners via `scripts/cloud-build/`:

```bash
# Validate gh auth and preview pipeline
./scripts/cloud-build/cloud-compile.sh --dry-run --fork "$(gh api user -q .login)/Anywhere"

# Full loop: inject workflow → dispatch → watch → download Anywhere.ipa
./scripts/cloud-build/cloud-compile.sh --fork "$(gh api user -q .login)/Anywhere"
```

**Requirements:** `gh` authenticated with `repo` + `workflow` scopes (integration tokens may inject files but cannot `workflow_dispatch`).

| Script | Role |
| --- | --- |
| `cloud-compile.sh` | Main orchestrator |
| `lib/gh-auth.sh` | Validates `gh`; falls back to `gh auth login` |
| `lib/fork.sh` | Idempotent fork of `NodePassProject/Anywhere` |
| `lib/github-api.sh` | Base64 Contents API `PUT` for `.github/workflows/build.yml` |
| `lib/workflow-watch.sh` | `workflow_dispatch`, `gh run watch`, artifact download |
| `workflows/build.yml` | macOS build template (also at `.github/workflows/build.yml`) |

Build flags: Xcode 15.4, `Release` + `iphoneos`, `CODE_SIGNING_ALLOWED=NO`, artifact `Anywhere-Unsigned-IPA`.
178 changes: 97 additions & 81 deletions Anywhere/Views/ProxyList/ProxyListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,106 +33,122 @@ struct ProxyListView: View {
}

var body: some View {
List {
Section {
ForEach(standaloneItems) { item in
proxyRow(item, editingDisabled: false)
proxyList
.overlay { emptyOverlay }
.navigationTitle("Proxies")
.toolbar {
if standaloneItems.count > 1 || subscriptionStore.subscriptions.count > 1 {
ToolbarItemGroup { reorderLink }
}
}
ForEach(subscriptionStore.subscriptions) { subscription in
let editingDisabled = SubscriptionDomainHelper.shouldDisableProxyEditing(for: subscription.url)
Section {
if !collapsedSubscriptions.contains(subscription.id) {
ForEach(items(for: subscription)) { item in
proxyRow(item, editingDisabled: editingDisabled)
}
}
} header: {
subscriptionHeader(subscription)
if #available(iOS 26.0, *) {
ToolbarSpacer()
}
}
}
.overlay {
if configStore.configurations.isEmpty {
ContentUnavailableView("No Proxies", systemImage: "network")
}
}
.navigationTitle("Proxies")
.toolbar {
if standaloneItems.count > 1 || subscriptionStore.subscriptions.count > 1 {
if #available(iOS 27.0, *) {
ToolbarItemGroup {
NavigationLink {
ReorderProxiesView()
} label: {
Label("Reorder Proxies", systemImage: "arrow.up.arrow.down")
}
ToolbarItemGroup {
Button(action: testAllVisibleLatencies) {
Label("Test All", systemImage: "gauge.with.dots.needle.67percent")
}
.visibilityPriority(.low)
} else {
ToolbarItemGroup {
NavigationLink {
ReorderProxiesView()
} label: {
Label("Reorder Proxies", systemImage: "arrow.up.arrow.down")
}
Button { showingAddSheet = true } label: {
Label("Add", systemImage: "plus")
}
}
}

if #available(iOS 26.0, *) {
ToolbarSpacer()
.sheet(isPresented: $showingAddSheet) { addProxySheet }
.sheet(isPresented: $showingManualAddSheet) { manualAddSheet }
.sheet(item: $configurationToEdit) { configuration in editProxySheet(configuration) }
.alert("Update Failed", isPresented: $showingSubscriptionError) {
Button("OK", role: .cancel) { }
} message: {
Text(subscriptionErrorMessage)
}

ToolbarItemGroup {
Button {
let visible = configStore.configurations.filter { configuration in
guard let subId = configuration.subscriptionId else { return true }
return !collapsedSubscriptions.contains(subId)
.alert("Rename", isPresented: renameBinding) {
TextField("Name", text: $renameText)
Button("OK") {
if let subscription = renamingSubscription, !renameText.isEmpty {
subscriptionStore.rename(subscription, to: renameText)
}
viewModel.testLatencies(for: visible)
} label: {
Label("Test All", systemImage: "gauge.with.dots.needle.67percent")
}
Button {
showingAddSheet = true
} label: {
Label("Add", systemImage: "plus")
Button("Cancel", role: .cancel) { }
}
.onAppear {
collapsedSubscriptions = Set(subscriptionStore.subscriptions.filter(\.collapsed).map(\.id))
}
}

@ViewBuilder
private var proxyList: some View {
List {
Section {
ForEach(standaloneItems) { item in
proxyRow(item, editingDisabled: false)
}
}
}
.sheet(isPresented: $showingAddSheet) {
DynamicSheet(animation: .snappy(duration: 0.3, extraBounce: 0)) {
AddProxyView(showingManualAddSheet: $showingManualAddSheet)
ForEach(subscriptionStore.subscriptions) { subscription in
subscriptionSection(subscription)
}
}
.sheet(isPresented: $showingManualAddSheet) {
ProxyEditorView { configuration in
configStore.add(configuration); viewModel.selectIfNone(configuration)
}

@ViewBuilder
private func subscriptionSection(_ subscription: Subscription) -> some View {
let editingDisabled = SubscriptionDomainHelper.shouldDisableProxyEditing(for: subscription.url)
Section {
if !collapsedSubscriptions.contains(subscription.id) {
ForEach(items(for: subscription)) { item in
proxyRow(item, editingDisabled: editingDisabled)
}
}
} header: {
subscriptionHeader(subscription)
}
.sheet(item: $configurationToEdit) { configuration in
ProxyEditorView(configuration: configuration) { updated in
configStore.update(updated)
}
}

@ViewBuilder
private var emptyOverlay: some View {
if configStore.configurations.isEmpty {
ContentUnavailableView("No Proxies", systemImage: "network")
}
.alert("Update Failed", isPresented: $showingSubscriptionError) {
Button("OK", role: .cancel) { }
} message: {
Text(subscriptionErrorMessage)
}

private var reorderLink: some View {
NavigationLink {
ReorderProxiesView()
} label: {
Label("Reorder Proxies", systemImage: "arrow.up.arrow.down")
}
.alert("Rename", isPresented: Binding(get: { renamingSubscription != nil }, set: { if !$0 { renamingSubscription = nil } })) {
TextField("Name", text: $renameText)
Button("OK") {
if let subscription = renamingSubscription, !renameText.isEmpty {
subscriptionStore.rename(subscription, to: renameText)
}
}
Button("Cancel", role: .cancel) { }
}

private var addProxySheet: some View {
DynamicSheet(animation: .snappy(duration: 0.3, extraBounce: 0)) {
AddProxyView(showingManualAddSheet: $showingManualAddSheet)
}
.onAppear {
collapsedSubscriptions = Set(subscriptionStore.subscriptions.filter(\.collapsed).map(\.id))
}

private var manualAddSheet: some View {
ProxyEditorView { configuration in
configStore.add(configuration)
viewModel.selectIfNone(configuration)
}
}

private func editProxySheet(_ configuration: ProxyConfiguration) -> some View {
ProxyEditorView(configuration: configuration) { updated in
configStore.update(updated)
}
}

private var renameBinding: Binding<Bool> {
Binding(
get: { renamingSubscription != nil },
set: { if !$0 { renamingSubscription = nil } }
)
}

private func testAllVisibleLatencies() {
let visible = configStore.configurations.filter { configuration in
guard let subId = configuration.subscriptionId else { return true }
return !collapsedSubscriptions.contains(subId)
}
viewModel.testLatencies(for: visible)
}

// MARK: - Subscription Header
Expand Down
Loading