Skip to content

Add per-client external link controls#5650

Draft
fastnas2023 wants to merge 5 commits into
MHSanaei:mainfrom
fastnas2023:pr/external-link-controls
Draft

Add per-client external link controls#5650
fastnas2023 wants to merge 5 commits into
MHSanaei:mainfrom
fastnas2023:pr/external-link-controls

Conversation

@fastnas2023

Copy link
Copy Markdown

Summary

Adds per-client controls for external links and external subscriptions without changing WireGuard client binding architecture.

  • add enable/disable support for each external link/subscription row
  • add optional expiryTime and namePrefix metadata
  • track lastFetchAt and lastFetchError for remote external subscriptions
  • keep disabled/expired rows visible in the client editor while excluding them from subscription output
  • update API docs/OpenAPI and client form UI

Notes

This is intentionally split from the earlier WireGuard client-binding PR. It does not change the main WireGuard clients[] model on current main.

Verification

  • go test ./internal/sub ./internal/web/service
  • npm --prefix frontend run build
  • go test ./...
  • git diff --check

@github-actions github-actions Bot added enhancement New feature or request go Pull requests that update Go code javascript Pull requests that update javascript code labels Jun 29, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well-structured feature with solid Go test coverage. There is one blocking correctness issue in how pre-existing rows interact with the new expiry filter, and the i18n rollout is incomplete (a documented hard requirement). The four inline comments below carry the details.

Blocking

  • internal/sub/external_config.go:59 — the new expiry_time predicate is not NULL-tolerant, so external-link rows that existed before this migration (backfilled as NULL) are dropped from every subscription.
  • i18n — the five new pages.clients.* keys are added only to en-US and zh-CN; the other 11 locale files lack them.

Suggestions

  • internal/database/model/model.go:714expiry_time / last_fetch_at omit the default:0 tag used for every other integer column.
  • internal/sub/external_subscription.go:52 — fetch state is written to the DB on every cache hit, turning the subscription read path into a write.

To verify before merge

  • Confirm make gen reproduces frontend/public/openapi.json with no further diff; the file is generated from endpoints.ts, not hand-maintained.
  • The go test ./... / npm run build runs listed in the PR description.

This review was generated automatically; a maintainer may follow up.

now := time.Now().UnixMilli()
if err := db.Where("client_id IN ?", clientIds).
Where("(enable IS NULL OR enable = ?)", true).
Where("(expiry_time = 0 OR expiry_time > ?)", now).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: The enable predicate above is NULL-tolerant (enable IS NULL OR enable = ?), but this expiry_time predicate is not. The expiry_time column is added without default:0 (unlike every other integer column in the schema), so on existing databases AutoMigrate adds it nullable and rows created before this migration are backfilled as NULL. Under SQL three-valued logic expiry_time = 0 OR expiry_time > ? is false for NULL, so every pre-existing external link silently disappears from all client subscriptions after upgrade. Make the predicate NULL-tolerant (and/or add a backfill mirroring normalizeClientExternalLinkEnable).

Suggested change
Where("(expiry_time = 0 OR expiry_time > ?)", now).
Where("(expiry_time IS NULL OR expiry_time = 0 OR expiry_time > ?)", now).

Value string `json:"value" gorm:"column:value"`
Remark string `json:"remark" gorm:"column:remark"`
Enable *bool `json:"enable" gorm:"column:enable;default:true"`
ExpiryTime int64 `json:"expiryTime" gorm:"column:expiry_time"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Every other integer column that needs a defined value carries gorm:"...;default:0" (for example SortOrder, Reset, Port, Priority). expiry_time and last_fetch_at omit it, so AutoMigrate adds them as nullable columns and pre-existing rows are backfilled as NULL rather than 0. Adding the default is the cleaner root-cause fix for the expiry-filter issue flagged in external_config.go.

Suggested change
ExpiryTime int64 `json:"expiryTime" gorm:"column:expiry_time"`
ExpiryTime int64 `json:"expiryTime" gorm:"column:expiry_time;default:0"`

Consider the same ;default:0 on last_fetch_at (line 716).

Comment thread internal/sub/external_subscription.go Outdated
cached, ok := subscriptionCache.m[rawURL]
subscriptionCache.Unlock()
if ok && time.Since(cached.fetchedAt) < subscriptionCacheTTL {
updateExternalSubscriptionFetchState(rowID, cached.fetchedAt, nil)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: updateExternalSubscriptionFetchState issues a DB UPDATE here on every cache hit, repeatedly writing the same cached.fetchedAt. Because subscriptions are polled frequently and one request can carry several subscription rows, this converts the previously read-only subscription-serving path into a write path; on SQLite writes serialize, so it adds lock contention under load. Consider recording fetch state only when it actually changes — for example drop the call on the cache-hit branch and keep the writes on the real-fetch success/error branches.

"addExternalSubscription": "Add External Subscription",
"noExternalLinks": "No external links yet.",
"noExternalSubscriptions": "No external subscriptions yet.",
"namePrefix": "Name prefix",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking: These five keys (namePrefix, lastFetchAt, lastFetchError, neverFetched, leaveBlankToNeverExpire) are added under pages.clients here and in zh-CN.json, but the other 11 files in internal/web/translation/ do not define them under pages.clients. Per the project hard rules a new English key must be added to every locale JSON. Missing keys fall back to en-US so the build still passes, but non-English/Chinese users see English text. Add the translated keys to ar-EG, es-ES, fa-IR, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, and zh-TW. Note leaveBlankToNeverExpire already exists in those files but only under pages.inbounds, so the pages.clients lookup still misses.

@MHSanaei MHSanaei marked this pull request as draft June 29, 2026 10:55
The subscription render path issued an UPDATE even on cache hits (write amplification on a hot path); now writes only on a real fetch or error. Adds the 5 new keys to the remaining 11 locales and trims added Go comments. Merges current main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request go Pull requests that update Go code javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants