Currently, if system /usr/share/v2raya/geosite.dat is used, v2raya will
not post error, but will display "none" for local GFWList date.
Since v2rayA reads this compatible file, we should display a date for
it, so that user is not confused.
Assisted-by: GPT-5.6-terra
The DNS form's placeholder offered https://dns.google/dns-query, the
service turned https:// into protocol "https" and quic:// into "quic",
and the DNS module handed those to a miekg/dns client, which speaks udp,
tcp and tcp-tls only. Saving succeeded and every query on that rule then
failed, visible only in the log.
CheckDnsUpstream in kernel/v2ray accepts an address, tcp://, udp:// and
tls:// (DNS over TLS, which the module does support) and rejects https://
and quic:// with a message that names the alternatives; PutDnsRules
returns it as a 400 before anything is stored. The placeholder in the six
locales shows the supported forms and the docs say which schemes work.
Verified on the VM: https:// and quic:// rules come back BAD_REQUEST with
the message, tls://1.1.1.1 saves and the module logs
addr=1.1.1.1:853 protocol=tcp-tls; the previous rules restore unchanged.
The documentation site's domain has expired and its pages described a
2021 v2rayA. The page now carries its own docs, written against the
current code and shipped with the build, so what a user reads matches the
binary they run: quick start, transparent proxy, routing rules, inbounds
and sharing, flags and environment, troubleshooting. English and
Simplified Chinese; the other locales read the English text and say so.
The sections are Markdown under gui/src/docs/<locale>/, compiled to HTML
at build time by a small vite plugin around marked (a devDependency; the
bundle carries no parser) and loaded on demand, so the first load does not
grow. Headings get ids from their text. The flag table is not written by
hand: GET /api/params reads conf.Params' struct tags (id, short, default,
desc) through reflection, the same tags gonfig parses, and the page renders
it, so a new flag documents itself.
"#docs" and "#docs/<section>" open the page; the RoutingA dialog's help
button, the About page and small book icons in the settings sections and
the inbound dialogs point there instead of at the dead site or the wiki.
The navigation gains a fifth destination.
Deleting nodes stored the renumbered connections, the subscriptions and
the servers in three commits; they are one transaction. Without a
catch-all DNS rule the module keeps its own fallback, as every earlier
release did. The login body is capped at 4 KiB, since it is now read
before the one-at-a-time semaphore. The apply-config error names a
failed restore, so every handler's message says whether the previous
value came back. The Windows system proxy reuses a snapshot pending
from an earlier run instead of refusing to start.
PostImport, PutPorts, PutDnsRules, PutRoutingA, PutGFWList,
DeleteGFWList, PutDomainsExcluded, PutTproxyWhiteIpGroups, PostOutbound
and PutOutbound wrote the store and regenerated the core outside the
mutation turn, so two of them could interleave core restarts with each
other or with a latency test. They take it like the rest.
ParseSSURL and ParseVmessURL sliced past the end of a link shorter than
its scheme prefix ("ss:", "vmess:x"), a panic reachable from a
subscription line.
A custom latency test URL that http.NewRequest rejects made a spawned
goroutine dereference nil and exit the process; the URL is validated
before any goroutine starts and the request error is handled. A
Shadowsocks link whose plugin field has no ";" panicked the parser, and
from the automatic subscription update that goroutine was unrecovered;
the parser accepts it and each updater recovers and logs.
The DNS module took the last non-bootstrap upstream as the fallback
while the template emitted upstreams in rule order with deduplication,
so a catch-all rule anywhere but last, or one whose upstream was
deduplicated, was not the fallback. The template names the fallback in
a new default_upstream field and the module honours it.
PUT /routingA stored the rules without regenerating the running core.
It goes through ApplyCoreConfig like the other config writers, with
restore on failure. UpdateSubscription logged a failed core reload and
answered success; it returns the error. PATCH /subscription joins the
mutation turn.
Subscription fetching retried through a direct connection when the
selected proxy failed, leaking the subscription URL; it returns the
error. Subscription documents are capped at 32 MiB and downloaded
assets at 256 MiB. PUT /gfwList validates the downloaded file as a
geosite list before replacing the working one.
The login handler took its one-at-a-time semaphore before reading the
body, so a client sending headers and no body blocked every login; the
body is read first and the HTTP server has header and idle timeouts.
Share links: a VLESS or VMess link without a numeric port is rejected
at parse time instead of producing port 0; an SSR link with a password
that is not base64 is rejected instead of importing empty; the SIP003
path gets its leading slash prepended, not a trailing one appended.
The macOS system proxy quoted shell arguments with strconv.Quote,
which leaves $(...) live; values are single-quoted. The Windows system
proxy snapshot includes AutoConfigURL, which teardown deleted and never
restored. Latency test responses with a non-2xx status are closed. The
version response carries the docker flag the GUI's port hints read.
Three structural changes that share files, so one commit:
Settings, ports, DNS rules and custom inbounds each carried their own
copy of "store the new value, regenerate the core config, put the old
value back when that fails", and the outbound setting path had no
restore at all. ApplyCoreConfig takes a snapshot function and a store
function and does the sequence once for all five callers; the outbound
setting path restores like the others, and the error names which of
the three steps failed.
configure.Which was the persisted discriminator, the key that locates a
node, and the response row with its latency, link and selected flag at
once. NodeRef {type, id, sub, outbound} is the identity used by the
locator, EqualTo, the connected lists and the group operations; Which
embeds it and keeps the status fields. The JSON stays byte for byte
what it was (a test marshals a fixture before and after), stored
connected lists decode unchanged, and deduplication keys by identity
instead of by the whole row.
db.List* dispatched on "bucket/key" string literals to reach the
servers and subscriptions tables. The bodies are now Servers* and
Subscriptions* functions that configure calls directly; the List*
wrappers remain for the migration.
Three service packages outside the executable's dependency graph
(common/antiPollution, pkg/util/gopeed, kernel/vmessInfo), a blank
import of miekg/dns the core already imports, the --core-type flag no
code read, the PutAccount handler whose route was commented out, two GUI
API wrappers without a caller, two .SRCINFO files the release regenerates
and an unreferenced runit sample. The workflow path filters watched a
root go.mod that does not exist and the PR build skipped core/.
The core hooks ran under exec.Command with no deadline while Start held
the manager's lock, so a hook that never returned left the proxy stopped
and every status read blocked. They now run with the startup timeout as
their deadline in their own process group.
The custom-inbound POST and DELETE handlers did not take the mutation
turn; two concurrent additions read the same list and the last write
won. They take it like every other state-changing handler.
A failed HTTP bind ended in log.Fatal before the deferred cleanup, so
the core and the transparent-proxy rules stayed behind; run() now
returns the error through the same cleanup the shutdown path uses.
The resolv.conf backup was written in place; a crash mid-write left a
truncated file that the next start restored as the original. It is
written to a temporary file, synced and renamed, and an empty backup is
ignored on restore.
One `updating` flag guarded thirteen handlers, reads included: while a
subscription update or a latency test ran, GET /touch answered
REQUEST_IN_PROGRESS, so refreshing the page during a test failed at
random. A touch is a database read and needs no turn.
The flag becomes a turn that only the state-changing handlers take, and
a handler that finds it taken waits up to five seconds before answering
REQUEST_IN_PROGRESS instead of refusing at once; a client that leaves
while waiting is not answered.
An outbound group with several members is balanced by the observatory
(least ping). The group's setting now also names one member's share
link as Selected: the core template then keeps only that member, the
touch flags it on the connected entry, and PUT /outboundSelection sets
or clears it. A selection that matches no member is ignored, so a node
removed or renamed by a subscription update returns the group to
balancing instead of breaking it.
PUT /dnsRules regenerates the running core's config and restores the
previous rules when that fails; SetPorts restores the previous ports
when the restart fails. The HTTP latency test drops subscription
entries as intended. The remote GFWList version request has a deadline
and its cache a one-hour TTL. CheckUpdate picks the index of the running
architecture and closes the body on every path.
transparentType=tun no longer starts the TinyTun process. The template
emits a tun-mips inbound tagged transparent — device name, address, MTU,
the DNS module's address as the relay target, the user's exclusion list
plus v2rayA and the core by name and v2rayA by PID — and the service does
what the operating system needs once the core has the device up.
On Linux that is one table and four policy rules: table 2020 holds the
default route into tun0; marked packets (0x80: the core's sockets, the
direct outbound, the DNS module's upstreams) look up main in full and
leave through the physical interface, or fail with ENETUNREACH when main
has no route for their family rather than fall into the TUN; unmarked
packets take main with its default route suppressed, so LAN and static
routes stay physical, and everything else goes to table 2020. The main
table is never edited, so stopping restores egress by itself. A rule in
our preference range or a route in our table that is not ours is reported
and left alone; our own leftovers from a crash are removed first. Every
command that succeeded is recorded and undone in reverse on stop.
Three things the TinyTun path never hit are now on tun's path, so they
are fixed here: the DNS module is generated in tun mode (TinyTun did DNS
itself); its 127.2.0.17:53 listener is only added when port 53 is free,
instead of making the core exit on a host running another resolver; its
primary listener stays on loopback unless port sharing is on or the user
chose an address, and the DNS REDIRECT rules and their cleanup follow the
module's port instead of a hard-coded 52353, and a ::1 listener is added
when IPv6 is supported, because ip6tables REDIRECT delivers an
application's IPv6 query to ::1 which the IPv4 listener never saw. The
tun branch no longer
skips those rules: they are what catches queries to a LAN resolver, which
the routing keeps off the TUN.
The connectivity monitor probes the core's API port; the TinyTun socks
loop-back port it used is gone. Removed with TinyTun: --tinytun-bin, the
tunProcessBackend (eBPF) and tunBypassInterfaces settings (connected and
static routes bypass the TUN unconditionally now), the /networkInterfaces
endpoint that only served the latter, and the build tag. The version
endpoint reports tunSupported instead of tinytunSupported.
Tests cover the generated inbound and DNS module, the listen-address
rules, the redirect port, and — with V2RAYA_CORE_BIN set — that the
generated document loads in the real core. Windows and macOS route
handling follows in a separate commit; until then those builds report
the TUN as unsupported.
Without --log-file the viewer said "log printed to console, please see log in
console", which the Windows service, the launchd job and every "log to
console" setup cannot do. The logger now also keeps its last 4 MiB in memory
and the viewer reads that tail when there is no file; the lines carry the
same timestamp and level prefix as the file adapter's.
The log area also gave way to nothing: at 50vh a short window pushed the
filters and the export button below the fold with only a scrollbar to hint at
them. It now shrinks so the footer stays visible.
Three endpoints answered FAIL for something the caller asked for and got:
- an up-to-date GFWList came back as "could not update GFWList: latest version
is …. GFWList is up to date", because the check returned an error;
- deleting the GFWList when it was already gone returned the raw
"remove …: no such file or directory";
- the update check said "failed to get latest version from Package file: 1".
The up-to-date case is now a sentinel error the controller turns into a
success carrying alreadyUpToDate, deleting what is already gone succeeds, and
the two package-index messages say what was missing. "which can not be nil"
also became a sentence about the request.
A custom inbound port was open to anyone who could reach it, and older v2rayA
versions did offer authenticated inbounds, so users who relied on them had no
replacement (v2rayA/v2rayA discussions #1894, #1907).
CustomInbound carries optional credentials, which become the inbound's account
list. Half a credential pair is rejected: it would silently leave the port open.
Three gaps the settings rollback left open:
- adding or deleting a custom inbound stored the new list and then reloaded the
core; a core that refused the list stayed stopped with the rejected inbound
stored, so every later start failed too;
- a settings save that flipped ip forwarding left the kernel flag on the new
value after the setting itself was rolled back, so the stored setting and the
host disagreed;
- the rollback reported "the previous ones are back" even when restoring the
setting or restarting the core had failed as well.
The session secret now retries persistence instead of accepting a failed store
for the life of the process, and an unreadable secret row is no longer
overwritten with a fresh one, which would have signed out every session.
UpdateSetting stored the new setting before asking the core to reload it. When
the core refused the new config, the controller stopped the core and left the
rejected setting in the database, so the proxy was down and the next save
reported success while nothing was running.
Snapshot the setting before storing it, and on a reload failure put it back and
restart the core with it. The response now carries INVALID_CONFIG and the
reason the core gave.
The GUI shows backend error text as is, so errors were never localized.
common.Coded wraps an error with a stable code and scalar parameters;
ResponseError adds "errorCode" and "params" next to the unchanged English
"message" when the chain carries one. Thirty-two user-facing errors are
coded (no server selected, core start failure, port occupied, malformed
link with protocol and detail, subscription fetch failure with host,
server/subscription not found with ids, bad request with the field,
credentials, session, admin, request in progress, GFW list missing,
invalid config, asset download, transparent setup, mux range, custom
inbound fields). logError now keeps the error chain instead of
flattening it. Messages, status codes and control flow are unchanged; the
two strings the GUI matches on stay as they are.
tools_test.go becomes an external test package so the common tests
compile again; its FillEmpty equality assertion, which never ran before,
is skipped with the reason.
Errors that reach the GUI were written for the log: "ResolveURL failed:
invalid parameters", "httpGetUsingSpecificClient: response is nil",
"LocateServerRaw: invalid TYPE", "bad request", "illegal base64 data at
input byte 18". Reword the messages that can reach a user so each names
the input that was rejected (link scheme, missing password, port, server
id, request field), states the expected form for malformed input, drops
Go function names, keeps the %w chain so errors.Is/As still work, and
never echoes passwords, tokens or full share links (hosts only).
Messages the GUI string-matches ("invalid config", "the last request is
being processed") are unchanged.
Adding or deleting a custom inbound only wrote the database: the running
core kept its old inbounds until the next unrelated restart, and a port
that collided with the configured socks5/http/pac/vmess/api ports was
accepted and broke the next core start. Reject out-of-range and colliding
ports with a message naming the port, and regenerate the config when the
core is running.
PutSetting decoded the request into a zero Setting and persisted the whole
struct, so the fields the settings dialog never sends (dnsCacheEnabled,
dnsPrefetch, dnsNegativeCache, the per-protocol backends) were saved as
false or empty on the first Save and the DNS cache silently switched off
with no control in the GUI to switch it back. Decode over the stored
setting so absent fields keep their value.
- time.Ticker.Reset panics on a non-positive duration; a stored auto-update
interval of 0 crashed every start (pre_update.go) and every settings
save. Route both tickers through configure.IntervalHours, which falls
back to one hour.
- The auto-updater indexes a startup snapshot; a subscription deleted
while it runs panicked out of range in a goroutine and took the daemon
down. UpdateSubscription now refuses an index that no longer exists.
- ResolveSubscriptionWithClient applied its 30 s timeout floor to a copy
it never used, accepted HTTP error pages as an empty node list, and
returned zero nodes without error, so UpdateSubscription replaced the
stored list with nothing on a provider 403/5xx. Use the copy, reject
status >= 400 and empty results.
- PutOutbound wrote an error response and then fell through to the
success response, producing two JSON bodies.
- TestHttpLatency passed nil entries (servers it could not locate) to
addHosts, which dereferenced them; skip them.
- ReqCache read the per-request mutex map after releasing the lock that
guards writes to it (a fatal concurrent map access under load).
- The websocket writer ran one goroutine per message box on a single
gorilla connection, which forbids concurrent writes; serialize them.
Import guessed from the URL scheme. Commit 3fd0e882 added http and https
to the server prefixes so that http proxy nodes were not stored as
subscriptions, and from then on every http(s) subscription address was
parsed as a proxy node and failed with "ResolveURL failed: invalid
parameters".
POST /api/import now takes "kind": "server" or "subscription". Import is
split into ImportServer and ImportSubscription; the old scheme-guessing
Import remains for requests without kind, with http and https back on
the subscription side as before 3fd0e882. The v4 migration calls the
explicit functions since it knows which list each address came from.
The GUI side (a server link / subscription address toggle that sends
kind) is in the gui branch.
core: fix geosite.dat parsing in DNS resolver
- Replace broken GeoSiteList unmarshal with correct varint-length-delimited
stream parser matching xray-core's format
- Add result caching to avoid re-reading the file per tag expansion
service: persist kernel exit status for proper state restoration
- Add LastKernelExitStatus (running/stopped/crashed) to DB
- Set 'running' on graceful shutdown while kernel is active so it
is automatically restored on next startup
- Set 'stopped' when user explicitly stops the kernel via API
- Set 'crashed' when the kernel exits unexpectedly
- Check saved status at startup and log appropriate messages
- Expose lastKernelExit field via GET /api/version for the frontend
Since v2rayA now exclusively uses the v2raya_core binary, simplify or
remove all logic that detected, compared, or branched on different
core types (v2ray, xray, merged).
- Remove CheckCoreVersion, CoreVersionMismatchError, and the
CoreVersion check in StartCoreProcess — core v2raya version
comparison is no longer needed.
- Remove DetectCoreTypeByBinaryName and the Unknown/V2ray/Xray/Merged
variant aliases. The only remaining Variant constant is V2rayaCore.
- Simplify GetV2rayServiceVersion: drop all CoreType/env-config
detection and variant mismatch verification. The function always
returns V2rayaCore and the version string from the binary.
- Simplify CheckV5 / isVersionSatisfied to return only an error,
dropping the unused Variant return value.
- Remove variant-branching in Shadowsocks and Trojan Configuration
methods — both always use the MT (Xray-compatible) path.
- Remove the CheckCoreVersion call from version.go and hardcode
coreVersionValid/ variant in the API response.
- Drop the now-unused where import from shadowsocks.go and trojan.go.