🌈 Sync 2026-11-14 02:21:00

This commit is contained in:
github-actions[bot] committed 2026-11-14 02:21:00 +08:00
1 parent f1cab8a933
commit 24333bdf44
22 files changed
+18341 -65

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ PKG_SOURCE_URL:=https://github.com/ysurac/glorytun.git
PKG_SOURCE_VERSION:=203c5f2223d7d5146d659b51159b2f02c2480d15
PKG_NAME:=glorytun-udp
PKG_VERSION:=0.3.4
PKG_RELEASE:=6
PKG_RELEASE:=7
PKG_LICENSE:=BSD-2-Clause
PKG_LICENSE_FILES:=LICENSE
+27 -1
View File
@@ -36,6 +36,29 @@ validate_section() {
'dev:string'
}
# persist keeps the device, and the address set on it, once glorytun is
# disabled: the next VPN on that device (OpenVPN's tun0) got both, and
# omr-tracker saw its gateway down. Only this exact address is removed,
# the device may already belong to that VPN
release_addr() {
[ -n "$1" ] && [ -n "$2" ] && [ -n "$3" ] || return 0
ip addr del "$2" peer "$3" dev "$1" 2>/dev/null
return 0
}
stopped_instance() {
local enable dev localip remoteip
validate_section "${1}" || return 0
[ "${enable}" = "1" ] || release_addr "$dev" "$localip" "$remoteip"
}
# force_stop_vpn disables then stops, without a start; an enabled instance
# keeps its address across a restart
service_stopped() {
config_load glorytun-udp
config_foreach stopped_instance glorytun-udp
}
start_instance() {
local enable key host port listener proto mtuauto chacha dev mode
local persist kxtimeout timetolerance keepalive localip remoteip
@@ -47,7 +70,10 @@ start_instance() {
return 1
}
[ "${enable}" = "1" ] || return 1
[ "${enable}" = "1" ] || {
release_addr "$dev" "$localip" "$remoteip"
return 1
}
[ -n "${key}" ] || return 1
[ "${key}" != "secretkey" ] || return 1
+1 -1
View File
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=tracebox
PKG_VERSION:=0.4.4
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.bz2
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
@@ -1,10 +1,45 @@
pkt = ip{dst=to} / tcp{dst=65101} / MPCAPABLE
local function uci_get(key)
local f = io.popen("uci -q get " .. key)
local v = f and f:read("*l") or ""
if f then f:close() end
return v or ""
end
-- Probe the port of the proxy in use: 65101 is shadowsocks-libev's, a VPS
-- may not run it or the path may treat it differently than the real one.
local port
local proxy = uci_get("openmptcprouter.settings.proxy")
if proxy:match("^shadowsocks%-rust") then
port = uci_get("shadowsocks-rust.sss0.server_port")
elseif proxy == "shadowsocks" then
port = uci_get("shadowsocks-libev.sss0.server_port")
end
port = tonumber(port) or 65101
-- tracebox's MPCAPABLE is MPTCP v0 (out-of-tree kernel), which the upstream
-- (v1, RFC 8684) kernel on the VPS ignores: build the 4-byte v1 SYN option
-- by hand (subtype 0 / version 1, flags H = HMAC-SHA256).
MPCAPABLE_V1 = TCPOption.new{kind=30, length=4, data={0x01, 0x01}}
pkt = ip{dst=to} / tcp{dst=port} / MPCAPABLE_V1
function cb(ttl, r_ip, mod)
if r_ip == nil then
r_ip = "*"
end
print(ttl .. ": " .. tostring(r_ip) .. " " .. tostring(mod))
-- print(tostring(mod:original():ip()))
mod = tostring(mod)
-- Crafter can't parse a 4-byte MP_CAPABLE quoted back in ICMP and shows
-- it as raw bytes: that's the option kept, not stripped. The server's
-- 12-byte SYN/ACK option likewise shows as "-TCPOption +...Capable".
if mod:find("+RawLayer", 1, true) or mod:find("+TCPOptionMPTCPCapable", 1, true) then
mod = mod:gsub("%-TCPOption ", ""):gsub("%+RawLayer ", "")
end
print(ttl .. ": " .. tostring(r_ip) .. " " .. mod)
end
print("Port: " .. port)
result = tracebox(pkt, {callback="cb"})
print("\nResult:\n" .. tostring(result))
print("\nResult:\n" .. tostring(result))
-- The page's help text and omr-tracebox-mptcp key on these option names.
if result == nil then
print("No reply from the server")
elseif not tostring(result):find("TCPOptionMPTCPCapable", 1, true) then
print("-TCPOptionMPTCPCapable: no MPTCP in the server reply")
end
+1 -1
View File
@@ -11,7 +11,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=v2raya-core
PKG_VERSION:=2.5.15
PKG_RELEASE:=2
PKG_RELEASE:=3
PKG_SOURCE:=v2rayA-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/v2rayA/v2rayA/tar.gz/v$(PKG_VERSION)?
-13
View File
@@ -1,13 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2026 Amr Fateem <amr.fateem@yahoo.com>
case "$1" in
v2raya-core)
/usr/bin/v2raya_core version | grep -F "$2"
;;
*)
exit 1
;;
esac
+13 -8
View File
@@ -14,12 +14,15 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=v2raya
PKG_VERSION:=2.5.15
PKG_RELEASE:=10
PKG_RELEASE:=11
PKG_SOURCE:=v2rayA-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/v2rayA/v2rayA/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=skip
WEB_FILE:=v2raya-web-$(PKG_VERSION).tar.gz
WEB_HASH:=76774f195ee74604fbc43a48fe74e5f3363e81ec68af02d1c26feeeacc0f63ae
PKG_BUILD_DIR:=$(BUILD_DIR)/v2rayA-$(PKG_VERSION)/service
PKG_LICENSE:=AGPL-3.0-only
@@ -117,12 +120,6 @@ define Package/v2raya/prerm
}
endef
# v2rayA publishes the built web UI as web.tar.gz on its releases. The
# GitHub release CDN has been returning 504/404 for it, which makes every
# matrix build fail. Keep the 871 KiB asset in the package instead; the
# hash of the upstream v2.5.13 asset is
# 76774f195ee74604fbc43a48fe74e5f3363e81ec68af02d1c26feeeacc0f63ae.
define Build/Prepare
$(call Build/Prepare/Default)
@@ -156,7 +153,7 @@ define Build/Prepare
( \
mkdir -p $(PKG_BUILD_DIR)/server/router/web ; \
gzip -dc $(CURDIR)/files/web.tar.gz | $(HOST_TAR) -C $(PKG_BUILD_DIR)/server/router/web $(TAR_OPTIONS) ; \
gzip -dc $(DL_DIR)/$(WEB_FILE) | $(HOST_TAR) -C $(PKG_BUILD_DIR)/server/router/web $(TAR_OPTIONS) ; \
)
endef
@@ -184,5 +181,13 @@ define Package/v2raya/install
$(INSTALL_DIR) $(1)/usr/share/v2raya
endef
define Download/v2raya-web
URL:=https://github.com/v2rayA/v2rayA/releases/download/v$(PKG_VERSION)/
URL_FILE:=web.tar.gz
FILE:=$(WEB_FILE)
HASH:=skip
endef
$(eval $(call Download,v2raya-web))
$(eval $(call GoBinPackage,v2raya))
$(eval $(call BuildPackage,v2raya))
+1
View File
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2021 Tianling Shen <cnsztl@immortalwrt.org>
# Copyright (C) 2021 Tianling Shen <cnsztl@immortalwrt.org>
# Copyright (C) 2026 Amr Fateem <amr.fateem@yahoo.com>
#
# procd init script for v2rayA.
Binary file not shown.
-13
View File
@@ -1,13 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2026 Amr Fateem <amr.fateem@yahoo.com>
case "$1" in
v2raya)
/usr/bin/v2raya --version | grep -F "$2"
;;
*)
exit 1
;;
esac
+83 -23
View File
@@ -1,23 +1,26 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=Xray-core
PKG_NAME:=xray-core
PKG_VERSION:=26.11.9
PKG_RELEASE:=30
PKG_RELEASE:=31
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/XTLS/Xray-core/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=skip
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
PKG_LICENSE:=MPL-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_LICENSE:=MPL-2.0 GPL-3.0-or-later MIT
PKG_LICENSE_FILES:=LICENSE proxy/anytls/LICENSE.sing-anytls proxy/naive/LICENSE.sing-box proxy/shadowsocksr/LICENSE proxy/tuic/LICENSE.mihomo proxy/snell/LICENSE.mihomo transport/internet/shadowtls/LICENSE.mihomo transport/internet/kcptun/LICENSE.mihomo transport/internet/kcptun/LICENSE.kcptun
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_DIR:=$(BUILD_DIR)/Xray-core-$(PKG_VERSION)
PKG_BUILD_DEPENDS:=golang/host upx/host
PKG_CONFIG_DEPENDS:=CONFIG_XRAY_CORE_NAIVE
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/xtls/xray-core
GO_PKG_LDFLAGS:=-s -w
GO_PKG_BUILD_PKG:=$(GO_PKG)/main
GO_PKG_LDFLAGS_X:= \
$(GO_PKG)/core.build=OpenWrt \
@@ -26,36 +29,93 @@ GO_PKG_LDFLAGS_X:= \
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/xray/template
# Cronet's native archives contain LLVM bitcode. Use its matching linker while
# retaining OpenWrt's target GCC, sysroot, ABI flags and C runtime.
CRONET_CLANG_VERSION:=llvmorg-24-init-3796-g20e97c4b-27
CRONET_CLANG_FILE:=clang-$(CRONET_CLANG_VERSION).tar.xz
CRONET_CLANG_DIR:=$(PKG_BUILD_DIR)/.cronet-clang
ifeq ($(CONFIG_XRAY_CORE_NAIVE),y)
GO_PKG_TAGS+=with_naive_outbound
ifeq ($(CONFIG_USE_MUSL),y)
GO_PKG_TAGS+=with_musl
endif
# golang-package.mk also appends TARGET_LDFLAGS through -extldflags, after
# CGO_LDFLAGS. Select LLD here so the SDK cannot override it with ld.bfd.
TARGET_LDFLAGS:=$(filter-out -fuse-ld=%,$(TARGET_LDFLAGS)) -B$(CRONET_CLANG_DIR)/bin -fuse-ld=lld $(if $(filter arm i386 mipsel loongarch64,$(ARCH)),-Wl$(comma)-no-pie) $(if $(filter loongarch64,$(ARCH)),-Wl$(comma)--no-relax)
ifeq ($(HOST_OS)-$(HOST_ARCH),Linux-x86_64)
define Download/cronet-clang
URL:=https://commondatastorage.googleapis.com/chromium-browser-clang/Linux_x64
URL_FILE:=$(CRONET_CLANG_FILE)
FILE:=$(CRONET_CLANG_FILE)
HASH:=skip
endef
$(eval $(call Download,cronet-clang))
endif
define Build/Prepare
@if [ "$(HOST_OS)-$(HOST_ARCH)" != "Linux-x86_64" ]; then \
echo "xray-core: Naive outbound requires a Linux x86_64 build host; disable CONFIG_XRAY_CORE_NAIVE on this host." >&2; \
exit 1; \
fi
$(call Build/Prepare/Default)
$(INSTALL_DIR) $(CRONET_CLANG_DIR)
$(TAR) -xJf $(DL_DIR)/$(CRONET_CLANG_FILE) -C $(CRONET_CLANG_DIR)
endef
endif
# Apply only to this package; runtime DT detection restricts use to AN7581.
# Resolve Go through the same versioned PATH and environment as the build.
ifeq ($(GO_ARCH),arm64)
define Build/Compile
$(GO_PKG_VARS) python3 $(PKG_BUILD_DIR)/tools/eip93/overlay.py --go go --out $(PKG_BUILD_DIR)/.eip93-overlay
$(call GoPackage/Build/Compile,-overlay=$(PKG_BUILD_DIR)/.eip93-overlay/overlay.json)
endef
endif
# Compress once per build, not once per package/rootfs installation.
# Keep Go's output intact and let the .built stamp guard the packed copy.
define Build/Install
$(INSTALL_DIR) $(PKG_BUILD_DIR)/.upx
$(INSTALL_BIN) $(GO_PKG_BUILD_BIN_DIR)/main $(PKG_BUILD_DIR)/.upx/xray
$(STAGING_DIR_HOST)/bin/upx --lzma --best $(PKG_BUILD_DIR)/.upx/xray
endef
# Go's -s -w flags strip the binary before UPX; do not strip it again.
RSTRIP:=:
define Package/xray-core
TITLE:=A platform for building proxies to bypass network restrictions
SECTION:=net
CATEGORY:=Network
URL:=https://xtls.github.io
endef
define Package/xray-core
$(call Package/xray/template)
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle
endef
define Package/xray/description
define Package/xray-core/config
config XRAY_CORE_NAIVE
bool "Enable NaiveProxy outbound (Chromium/Cronet)"
depends on PACKAGE_xray-core
depends on (aarch64 || arm || i386 || x86_64 || mipsel || riscv64 || loongarch64)
depends on (USE_MUSL || USE_GLIBC)
depends on !arm || (arm_v7 && HAS_FPU)
default $(if $(filter Linux-x86_64,$(HOST_OS)-$(HOST_ARCH)),y,n)
help
Link Chromium/Cronet for NaiveProxy HTTP/2 and HTTP/3 outbounds.
Requires an x86_64 Linux build host and adds a native library to Xray.
Disabled by default on other build hosts, including ARM and ARM64.
Naive inbound support remains available when this option is disabled.
endef
define Package/xray-core/description
Xray, Penetrates Everything. It helps you to build your own computer network.
It secures your network connections and thus protects your privacy.
endef
define Package/xray-core/description
$(call Package/xray/description)
endef
define Package/xray-core/conffiles
/etc/xray/
/etc/config/xray
endef
define Package/xray-core/install
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
$(INSTALL_DIR) $(1)/usr/bin/
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/main $(1)/usr/bin/xray
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/.upx/xray $(1)/usr/bin/xray
endef
$(eval $(call BuildPackage,xray-core))
+173
View File
@@ -0,0 +1,173 @@
# AN7581 EIP93 hybrid AES-GCM
Xray 26.10.31 includes an automatic AN7581-only AES-GCM path through Linux AF_ALG.
EIP93 performs AES-CTR; GHASH stays on the CPU. This is not native hardware GCM.
The ARM64 package build generates a private Go source overlay. It never changes
the shared toolchain. The overlay is checked against Go 1.27's implementation;
unknown source layouts fail the build so that they can be reviewed explicitly.
Go TLS/uTLS and other users of Go AES-GCM can use it. Native Cronet TLS does not.
TLS framing, nonce management, overlap checks and authentication checks remain
in their original implementations.
## Automatic selection and fallback
- Read the NUL-delimited root DT `compatible` property from sysfs or procfs.
- Enable only `airoha,an7581` / `airoha,en7581`, with no ARM AES instructions.
- Explicitly exclude AN7583/EN7583, other platforms, missing/malformed DT and FIPS.
- Use only standard 12-byte nonces, 16-byte tags and 4–32 KiB payloads.
- Bind `gcm_base(ctr(aes-eip93),ghash-generic)` on older kernels, retrying
`gcm_base(ctr(aes-eip93),ghash-lib)` on kernels such as 7.3 when the old
name fails to bind, including driver or policy errors. Both names require
the exact EIP93 CTR driver; failure of both keeps the original Go path.
- Verify each newly opened key against Go encryption/decryption and bad-tag
rejection before using it. Bound sockets, serialize each key's operations,
set socket timeouts and clear private buffers. The kernel must also be healthy;
socket timeouts do not recover a hung DMA engine.
- Missing modules, failed verification or syscall errors use the original Go
implementation. Authentication failures remain failures.
- `XRAY_EIP93=off` disables the optimization; there is no force-enable override.
The first successful activation prints:
`Xray EIP93: AN7581 AES-GCM hybrid backend enabled (AF_ALG CTR + CPU GHASH)`.
This message alone is not performance evidence; use engine interrupt counters.
Firmware needs `kmod-crypto-hw-airoha-eip93`, `kmod-crypto-gcm`, `kmod-crypto-user` and
GHASH/DES dependencies. The QWRT EIP93 package selects these dependencies.
BSP 5.4 needs the supplied driver and DT node. Linux 6.18 uses its native driver
with matching DMA cache-line and immutable-SA fixes. Linux 7.3 carries the
same protections plus the CTR next-IV carry fix, and additionally
needs the supplied AF_ALG exception for the exact EIP93 hybrid GCM driver;
leave `crypto.af_alg_restrict` at its default value. Native hardware authenc
is withheld because its bad-tag tests fail; hybrid GCM authenticates through
the kernel GCM implementation and passes its own bad-tag tests. AN7583 remains on the normal
Xray path until separate hardware measurements justify an alternative.
## Validation
The entire 100–190 patch stack replays on the official 26.10.31 archive with
`patch --fuzz=0`. The version upgrade refreshed dependency contexts and fixed
patch 100's TLS protobuf descriptor length; `infra/conf` tests catch that issue.
Existing protocol packages compile and their available tests pass. The ARM64
OpenWrt package also builds with `with_naive_outbound with_musl` and Cronet LLD.
This is not a claim of rerunning every historical cross-core protocol matrix.
On Nokia XG-040G-MD UBI / AN7581 / Linux 5.4.302:
- AES-128/192/256 comparisons against the independent Go generic implementation.
- Payload boundaries, in-place operations, corrupted tags, concurrent requests,
closed-socket fallback, nonstandard nonce/tag exclusion and the off switch.
- Go `crypto/cipher` short regression suite on the device.
- Ordinary client → optimized server and optimized client → ordinary server,
plus ordinary controls, using VLESS/TLS 1.2 AES-256-GCM. Each connection sends
and receives deterministic data and checks SHA-256. Hardware interrupts grow
only in optimized runs.
Local raw evidence and reproducible test sources:
`/home/lean/Desktop/airoha-5.4-evidence/eip93-20261022/`.
## Final Xray throughput
VLESS/TLS 1.2, server restricted to AES-256-GCM, same patched source and
Naive/Cronet build features. AN7581 at 1.2 GHz, Gigabit LAN. Each of two
alternating runs per row sends and receives 32 MiB and verifies SHA-256.
The optimized binary is the packaged UPX artifact; startup completes before
timing. Both ordinary-client/optimized-server and optimized-client/ordinary-
server directions passed. Values are mean Mbps; connection/relay overhead is
included. Receive results varied roughly 95–112 Mbps across optimized runs.
These final measurements use the driver extracted from the final IPK, including
the immutable-SA and native-authenc fixes; see `xray-e2e-packaged-final.log`.
| AN7581 role | Build | Router sends | Router receives |
| --- | --- | ---: | ---: |
| server | normal | 35.0 | 32.1 |
| server | eip93 | 136.6 | 98.2 |
| client | normal | 34.4 | 32.5 |
| client | eip93 | 133.3 | 104.5 |
All eight runs agree on SHA-256:
`e09320c5b00b34bb704802136c599a95b3996332ba84d7c7f21112b6231b6bd0`.
Ordinary runs add no EIP93 interrupts; each optimized run adds 8198.
This supports enabling the AN7581 path for bulk Go AES-GCM traffic, with
roughly 3–4x throughput versus ordinary Go. It does not imply gigabit GCM,
the same gain over optimized OpenSSL, or any measured benefit on AN7583.
The packaged binary with `XRAY_EIP93=off` also passed bidirectional hash
checks at 33.5/34.9 Mbps, with no EIP93 interrupt increase. The host AMD64
Go cipher regression passes with the overlay present and acceleration disabled.
The native-authenc quarantine preserves the kernel `authenc` composition over
EIP93 CBC/HMAC primitives. That composed implementation passes 12- and 13-byte
AAD encryption/decryption and corrupted-tag tests; `/proc/crypto` identifies
its module as `authenc`, rather than `crypto_hw_eip93`.
The final Nokia UBI firmware builds and includes Xray 26.10.21, EIP93, libdes,
GCM, GHASH and AF_ALG modules. Its embedded FIT DTB has the EIP93 node at
0x1fb70000 / GIC SPI 44. All 16 generated AN7581 BSP DTBs contain that node,
including standalone Nokia, Fiberhome and W1700K DTS layouts.
The final image passes `sysupgrade -T` on the Nokia test board
(`Signature check OK`). This validates the image without flashing it.
The board governor is restored to `schedutil` and test proxy processes stop
after each run. The final driver remains loaded temporarily for inspection.
## Linux 7.3 regression validation (2026-10-29)
On H3C HM2004-DU, five repeated hardware test runs passed AES-128/192/256-GCM,
boundary lengths, in-place operations, concurrent use, bad-tag rejection and
closed-socket fallback. CBC encrypt/decrypt/encrypt and CTR exact-rollover IV
tests failed with the original 7.3 driver and passed after the driver fixes.
The packaged Xray completed a 4 MiB AES-256-GCM Shadowsocks transfer with
matching SHA-256 and EIP93 activation at both ends. The disabled backend
completed the same transfer without increasing EIP93 interrupts.
`crypto.af_alg_restrict` remained at 1. These are correctness checks, not
throughput measurements. The legacy 5.4 name is retained; 5.4 hardware was
not rerun in this validation.
## Xray 26.10.29 regression validation (2026-10-31)
All ten patches were refreshed using temporary files and labeled unified diffs.
They replay on the official v26.10.29 archive with zero fuzz and no offsets.
The ARM64 OpenWrt package builds with Naive/Cronet enabled and passes the UPX
integrity check. Configuration, AnyTLS and VMess tests pass; the other added
protocol packages compile in the focused regression run.
On FiberHome HG5585F CT / AN7581 / Linux 7.3, the package was installed as
26.10.29-1 and its binary hash matched the IPK. The EIP93 hardware tests pass
AES-128/192/256, payload boundaries, in-place operations, bad-tag rejection,
concurrency and closed-socket fallback. A 4 MiB AES-256-GCM Shadowsocks
loopback transfer with both peers in the packaged Xray matches SHA-256
`2b07811057df887086f06a67edc6ebf911de8b6741156e7a2eb1416a4b8b1b2e`.
With `XRAY_EIP93=off`, the interrupt count stays at 1818; the enabled run
increases it to 2651, and unmodified `/sbin/usage` reports the same count.
`crypto.af_alg_restrict` remains 1.
These checks use the fixed EIP93 and AF_ALG modules loaded temporarily on the
test machine. They establish correctness and acceleration activation, not
throughput, a full firmware build or persistence of the kernel fixes after
reboot. The firmware must include the matching Linux 7.3 fixes.
## Xray 26.10.31 build validation (2026-11-01)
The source archive SHA-256 is
`be44f95d497ad4ba6e9d1dcfbbd71e8e51f812aff077d4c15c9947eff5e3046a`.
All ten patches replay with zero fuzz and zero offsets. Temporary files and
labeled unified diffs refresh the changed dependency contexts in patches 150
and 180, including upstream's protoc-gen-go-grpc update to 1.6.2.
EIP93 backend selection now tries the exact `ghash-lib` composition after any
legacy-name bind error, matching the Mihomo compatibility behavior. An ARM64
QEMU regression checks success, missing algorithms, invalid arguments, absent
drivers and permission errors, including failure of both names. DT gating and
the Go GCM tests pass; the hardware-only test is skipped without AN7581.
The ARM64 OpenWrt package builds with Naive/Cronet, musl and the private EIP93
Go overlay enabled. Configuration, AnyTLS and VMess tests pass, and the other
added protocol packages compile. The resulting ARM64 program starts under QEMU
and reports 26.10.31; the UPX integrity check passes.
This run does not repeat the historical board tests or validate Linux 7.3
hardware throughput. The matching kernel EIP93/AF_ALG fixes described above
remain required; native Cronet TLS does not use the Go acceleration overlay.
Local build and regression logs: `/tmp/xray-26.10.31-validation/`.
@@ -0,0 +1,154 @@
From: coolsnowwolf <coolsnowwolf@gmail.com>
Subject: [PATCH] tls: restore explicit allowInsecure compatibility
Restore the opt-in TLS option for legacy proxy certificates, including
Hysteria2 servers without SANs. Keep certificate verification enabled by
default and continue enforcing explicit certificate pins and names.
Signed-off-by: coolsnowwolf <coolsnowwolf@gmail.com>
--- a/infra/conf/transport_security.go
+++ b/infra/conf/transport_security.go
@@ -359,9 +359,7 @@
config.RejectUnknownSni = c.RejectUnknownSNI
config.MasterKeyLog = c.MasterKeyLog
- if c.AllowInsecure {
- return nil, errors.PrintRemovedFeatureError(`"allowInsecure"`, `"pinnedPeerCertSha256"(pcs) and "verifyPeerCertByName"(vcn)`)
- }
+ config.AllowInsecure = c.AllowInsecure
if c.PinnedPeerCertSha256 != "" {
for v := range strings.SplitSeq(c.PinnedPeerCertSha256, ",") {
v = strings.TrimSpace(v)
--- a/infra/conf/tls_allow_insecure_test.go
+++ b/infra/conf/tls_allow_insecure_test.go
@@ -0,0 +1,70 @@
+package conf_test
+
+import (
+ "crypto/sha256"
+ "crypto/tls"
+ "encoding/hex"
+ "encoding/json"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/xtls/xray-core/infra/conf"
+ xraytls "github.com/xtls/xray-core/transport/internet/tls"
+ "google.golang.org/protobuf/proto"
+)
+
+func TestTLSAllowInsecure(t *testing.T) {
+ server := httptest.NewUnstartedServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
+ server.Config.ErrorLog = log.New(io.Discard, "", 0)
+ server.StartTLS()
+ defer server.Close()
+ hash := sha256.Sum256(server.Certificate().Raw)
+ pin := hex.EncodeToString(hash[:])
+ for _, tc := range []struct {
+ name string
+ options string
+ wantOK bool
+ }{
+ {"default rejects untrusted certificate", `{}`, false},
+ {"false rejects untrusted certificate", `{"allowInsecure":false}`, false},
+ {"true accepts untrusted certificate", `{"allowInsecure":true}`, true},
+ {"valid pin", `{"pinnedPeerCertSha256":"` + pin + `"}`, true},
+ {"true with valid pin", `{"allowInsecure":true,"pinnedPeerCertSha256":"` + pin + `"}`, true},
+ {"true preserves invalid pin rejection", `{"allowInsecure":true,"pinnedPeerCertSha256":"` + strings.Repeat("00", 32) + `"}`, false},
+ {"true preserves explicit name verification", `{"allowInsecure":true,"verifyPeerCertByName":"wrong.invalid"}`, false},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ var input conf.TLSConfig
+ if err := json.Unmarshal([]byte(tc.options), &input); err != nil {
+ t.Fatal(err)
+ }
+ input.ServerName = "example.com"
+ built, err := input.Build()
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Exercise the protobuf path used by the core, not just the JSON loader.
+ data, err := proto.Marshal(built)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var config xraytls.Config
+ if err := proto.Unmarshal(data, &config); err != nil {
+ t.Fatal(err)
+ }
+ conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 3 * time.Second}, "tcp", server.Listener.Addr().String(), config.GetTLSConfig())
+ if conn != nil {
+ conn.Close()
+ }
+ if (err == nil) != tc.wantOK {
+ t.Fatalf("handshake error = %v, want success %v", err, tc.wantOK)
+ }
+ })
+ }
+}
--- a/transport/internet/tls/config.proto
+++ b/transport/internet/tls/config.proto
@@ -38,7 +38,8 @@
}
message Config {
- // Number 1 was assigned and used by an legacy option.
+ // Allow unverified certificates only when explicitly enabled by the client.
+ bool allow_insecure = 1;
// List of certificates to be served on server.
repeated Certificate certificate = 2;
--- a/transport/internet/tls/config.pb.go
+++ b/transport/internet/tls/config.pb.go
@@ -178,6 +178,8 @@
type Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
+ // Allow unverified certificates only when explicitly enabled by the client.
+ AllowInsecure bool `protobuf:"varint,1,opt,name=allow_insecure,json=allowInsecure,proto3" json:"allow_insecure,omitempty"`
// List of certificates to be served on server.
Certificate []*Certificate `protobuf:"bytes,2,rep,name=certificate,proto3" json:"certificate,omitempty"`
// Override server name.
@@ -241,6 +243,13 @@
return file_transport_internet_tls_config_proto_rawDescGZIP(), []int{1}
}
+func (x *Config) GetAllowInsecure() bool {
+ if x != nil {
+ return x.AllowInsecure
+ }
+ return false
+}
+
func (x *Config) GetCertificate() []*Certificate {
if x != nil {
return x.Certificate
@@ -385,8 +394,9 @@
"\x05Usage\x12\x10\n" +
"\fENCIPHERMENT\x10\x00\x12\x14\n" +
"\x10AUTHORITY_VERIFY\x10\x01\x12\x13\n" +
- "\x0fAUTHORITY_ISSUE\x10\x02\"\xca\x06\n" +
- "\x06Config\x12J\n" +
+ "\x0fAUTHORITY_ISSUE\x10\x02\"\xf1\x06\n" +
+ "\x06Config\x12%\n" +
+ "\x0eallow_insecure\x18\x01 \x01(\bR\rallowInsecure\x12J\n" +
"\vcertificate\x18\x02 \x03(\v2(.xray.transport.internet.tls.CertificateR\vcertificate\x12\x1f\n" +
"\vserver_name\x18\x03 \x01(\tR\n" +
"serverName\x12#\n" +
--- a/transport/internet/tls/config.go
+++ b/transport/internet/tls/config.go
@@ -385,6 +385,7 @@
PinnedPeerCertSha256: c.PinnedPeerCertSha256,
}
config := &tls.Config{
+ InsecureSkipVerify: c.AllowInsecure,
Rand: randCarrier,
ClientSessionCache: globalSessionCache,
RootCAs: root,
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,669 @@
From: coolsnowwolf <coolsnowwolf@gmail.com>
Subject: [PATCH] vmess: restore legacy alterId outbound authentication
Carry alterId through JSON, protobuf and memory accounts, restoring the
legacy HMAC-MD5 authentication and AES-CFB request/response headers for
explicitly configured nonzero alterId outbounds. Keep alterId=0 on AEAD
and reject legacy inbound settings, whose server support is not restored.
Initialize response keys before the concurrent request/response tasks.
Use the current timestamp, as Mihomo does, for legacy server compatibility.
Adapt the legacy client handshake from Xray v1.5.5. Cover alternative UUID
vectors, config/protobuf round trips, TCP/UDP payloads, both AEAD payload
ciphers and malformed legacy responses; retain existing AEAD header tests.
Reference: https://github.com/XTLS/Xray-core/blob/v1.5.5/proxy/vmess/encoding/client.go
Signed-off-by: coolsnowwolf <coolsnowwolf@gmail.com>
--- a/infra/conf/vmess.go
+++ b/infra/conf/vmess.go
@@ -17,6 +17,7 @@
type VMessAccount struct {
ID string `json:"id"`
+ AlterID uint16 `json:"alterId"`
Security string `json:"security"`
Experiments string `json:"experiments"`
}
@@ -35,7 +36,8 @@
st = protocol.SecurityType_AUTO
}
return &vmess.Account{
- Id: a.ID,
+ Id: a.ID,
+ AlterId: uint32(a.AlterID),
SecuritySettings: &protocol.SecurityConfig{
Type: st,
},
@@ -85,6 +87,10 @@
return errors.New("invalid VMess user").Base(err)
}
+ if account.AlterID != 0 {
+ return errors.New("legacy VMess alterId is supported for outbounds only")
+ }
+
u, err := uuid.ParseString(account.ID)
if err != nil {
return err
@@ -114,6 +120,7 @@
Level uint32 `json:"level"`
Email string `json:"email"`
ID string `json:"id"`
+ AlterID uint16 `json:"alterId"`
Security string `json:"security"`
Experiments string `json:"experiments"`
Receivers []*VMessOutboundTarget `json:"vnext"`
@@ -160,6 +167,7 @@
account := new(VMessAccount)
if c.Address != nil {
account.ID = c.ID
+ account.AlterID = c.AlterID
account.Security = c.Security
account.Experiments = c.Experiments
} else {
--- a/infra/conf/vmess_legacy_test.go
+++ b/infra/conf/vmess_legacy_test.go
@@ -0,0 +1,69 @@
+package conf_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "testing"
+
+ "github.com/xtls/xray-core/infra/conf"
+ "github.com/xtls/xray-core/proxy/vmess"
+ "github.com/xtls/xray-core/proxy/vmess/outbound"
+ "google.golang.org/protobuf/proto"
+)
+
+func TestVMessAlterIDConfig(t *testing.T) {
+ for _, aid := range []int{0, 1, 64} {
+ for _, flat := range []bool{false, true} {
+ t.Run(fmt.Sprintf("%d/flat=%v", aid, flat), func(t *testing.T) {
+ fields := fmt.Sprintf(`"id":"e641f5ad-9397-41e3-bf1a-e8740dfed019","alterId":%d`, aid)
+ raw := `{"address":"127.0.0.1","port":443,` + fields + `}`
+ if !flat {
+ raw = `{"vnext":[{"address":"127.0.0.1","port":443,"users":[{` + fields + `}]}]}`
+ }
+ var config conf.VMessOutboundConfig
+ if err := json.Unmarshal([]byte(raw), &config); err != nil {
+ t.Fatal(err)
+ }
+ message, err := config.Build()
+ if err != nil {
+ t.Fatal(err)
+ }
+ user, err := message.(*outbound.Config).Receiver.User.ToMemoryUser()
+ if err != nil {
+ t.Fatal(err)
+ }
+ account := user.Account.(*vmess.MemoryAccount)
+ if len(account.AlterIDs) != aid {
+ t.Fatalf("lost alterId: %d", len(account.AlterIDs))
+ }
+ wire, err := proto.Marshal(account.ToProto())
+ if err != nil {
+ t.Fatal(err)
+ }
+ var decoded vmess.Account
+ if err = proto.Unmarshal(wire, &decoded); err != nil {
+ t.Fatal(err)
+ }
+ if decoded.AlterId != uint32(aid) {
+ t.Fatal("protobuf round trip lost alterId")
+ }
+ })
+ }
+ }
+ for _, raw := range []string{`{"alterId":-1}`, `{"alterId":65536}`, `{"alterId":1.5}`} {
+ var c conf.VMessOutboundConfig
+ if err := json.Unmarshal([]byte(raw), &c); err == nil {
+ t.Fatalf("accepted %s", raw)
+ }
+ }
+ if _, err := (&vmess.Account{Id: "e641f5ad-9397-41e3-bf1a-e8740dfed019", AlterId: 65536}).AsAccount(); err == nil {
+ t.Fatal("accepted oversized protobuf alterId")
+ }
+ var inbound conf.VMessInboundConfig
+ if err := json.Unmarshal([]byte(`{"clients":[{"id":"e641f5ad-9397-41e3-bf1a-e8740dfed019","alterId":1}]}`), &inbound); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := inbound.Build(); err == nil {
+ t.Fatal("silently accepted unsupported legacy inbound")
+ }
+}
--- a/proxy/vmess/account.proto
+++ b/proxy/vmess/account.proto
@@ -12,6 +12,8 @@
// ID of the account, in the form of a UUID, e.g.,
// "66ad4540-b58c-4ad2-9926-ea63445a9b57".
string id = 1;
+ // Number of alternative IDs. Nonzero enables legacy outbound authentication.
+ uint32 alter_id = 2;
// Security settings. Only applies to client side.
xray.common.protocol.SecurityConfig security_settings = 3;
// Define tests enabled for this account
--- a/proxy/vmess/account.pb.go
+++ b/proxy/vmess/account.pb.go
@@ -27,6 +27,8 @@
// ID of the account, in the form of a UUID, e.g.,
// "66ad4540-b58c-4ad2-9926-ea63445a9b57".
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ // Number of alternative IDs. Nonzero enables legacy outbound authentication.
+ AlterId uint32 `protobuf:"varint,2,opt,name=alter_id,json=alterId,proto3" json:"alter_id,omitempty"`
// Security settings. Only applies to client side.
SecuritySettings *protocol.SecurityConfig `protobuf:"bytes,3,opt,name=security_settings,json=securitySettings,proto3" json:"security_settings,omitempty"`
// Define tests enabled for this account
@@ -72,6 +74,13 @@
return ""
}
+func (x *Account) GetAlterId() uint32 {
+ if x != nil {
+ return x.AlterId
+ }
+ return 0
+}
+
func (x *Account) GetSecuritySettings() *protocol.SecurityConfig {
if x != nil {
return x.SecuritySettings
@@ -90,9 +99,10 @@
const file_proxy_vmess_account_proto_rawDesc = "" +
"\n" +
- "\x19proxy/vmess/account.proto\x12\x10xray.proxy.vmess\x1a\x1dcommon/protocol/headers.proto\"\x91\x01\n" +
+ "\x19proxy/vmess/account.proto\x12\x10xray.proxy.vmess\x1a\x1dcommon/protocol/headers.proto\"\xac\x01\n" +
"\aAccount\x12\x0e\n" +
- "\x02id\x18\x01 \x01(\tR\x02id\x12Q\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\x12\x19\n" +
+ "\balter_id\x18\x02 \x01(\rR\aalterId\x12Q\n" +
"\x11security_settings\x18\x03 \x01(\v2$.xray.common.protocol.SecurityConfigR\x10securitySettings\x12#\n" +
"\rtests_enabled\x18\x04 \x01(\tR\ftestsEnabledBR\n" +
"\x14com.xray.proxy.vmessP\x01Z%github.com/xtls/xray-core/proxy/vmess\xaa\x02\x10Xray.Proxy.Vmessb\x06proto3"
--- a/proxy/vmess/account.go
+++ b/proxy/vmess/account.go
@@ -1,8 +1,12 @@
package vmess
import (
+ "bytes"
+ "crypto/md5"
"strings"
+ "github.com/xtls/xray-core/common/dice"
+
"google.golang.org/protobuf/proto"
"github.com/xtls/xray-core/common/errors"
@@ -14,6 +18,8 @@
type MemoryAccount struct {
// ID is the main ID of the account.
ID *protocol.ID
+ // AlterIDs are used only for legacy outbound authentication.
+ AlterIDs []*protocol.ID
// Security type of the account. Used for client connections.
Security protocol.SecurityType
@@ -40,6 +46,7 @@
}
return &Account{
Id: a.ID.String(),
+ AlterId: uint32(len(a.AlterIDs)),
TestsEnabled: test,
SecuritySettings: &protocol.SecurityConfig{Type: a.Security},
}
@@ -51,6 +58,9 @@
if err != nil {
return nil, errors.New("failed to parse ID").Base(err)
}
+ if a.AlterId > 65535 {
+ return nil, errors.New("VMess alterId exceeds 65535")
+ }
protoID := protocol.NewID(id)
var AuthenticatedLength, NoTerminationSignal bool
if strings.Contains(a.TestsEnabled, "AuthenticatedLength") {
@@ -61,8 +71,41 @@
}
return &MemoryAccount{
ID: protoID,
+ AlterIDs: newAlterIDs(id, a.AlterId),
Security: a.SecuritySettings.GetSecurityType(),
AuthenticatedLengthExperiment: AuthenticatedLength,
NoTerminationSignal: NoTerminationSignal,
}, nil
}
+
+// AnyValidID chooses an authentication ID without changing the primary command key.
+func (a *MemoryAccount) AnyValidID() *protocol.ID {
+ if len(a.AlterIDs) == 0 {
+ return a.ID
+ }
+ return a.AlterIDs[dice.Roll(len(a.AlterIDs))]
+}
+
+// newAlterIDs follows the original VMess alternative UUID derivation.
+func newAlterIDs(previous uuid.UUID, count uint32) []*protocol.ID {
+ if count == 0 {
+ return nil
+ }
+ ids := make([]*protocol.ID, count)
+ for i := range ids {
+ h := md5.New()
+ h.Write(previous.Bytes())
+ h.Write([]byte("16167dc8-16b6-4e6d-b8bb-65dd68113a81"))
+ var next uuid.UUID
+ for {
+ h.Sum(next[:0])
+ if !bytes.Equal(next.Bytes(), previous.Bytes()) {
+ break
+ }
+ h.Write([]byte("533eff8a-4113-4b10-b5ce-0f5d76b98cd2"))
+ }
+ ids[i] = protocol.NewID(next)
+ previous = next
+ }
+ return ids
+}
--- a/proxy/vmess/account_legacy_test.go
+++ b/proxy/vmess/account_legacy_test.go
@@ -0,0 +1,19 @@
+package vmess
+
+import "testing"
+
+func TestLegacyAlterIDDerivation(t *testing.T) {
+ a, err := (&Account{Id: "e641f5ad-9397-41e3-bf1a-e8740dfed019", AlterId: 2}).AsAccount()
+ if err != nil {
+ t.Fatal(err)
+ }
+ m := a.(*MemoryAccount)
+ for i, want := range []string{"5c1ef6b5-fd16-7081-c694-c2308d07559b", "283b417b-0e6e-eda7-50b7-fded0002659a"} {
+ if got := m.AlterIDs[i].String(); got != want {
+ t.Fatalf("ID %d: %s != %s", i, got, want)
+ }
+ }
+ if m.ID.String() != "e641f5ad-9397-41e3-bf1a-e8740dfed019" {
+ t.Fatal("primary ID changed")
+ }
+}
--- a/proxy/vmess/encoding/client.go
+++ b/proxy/vmess/encoding/client.go
@@ -3,6 +3,8 @@
import (
"bytes"
"context"
+ "crypto/hmac"
+ "crypto/md5"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
@@ -24,6 +26,7 @@
// ClientSession stores connection session info for VMess client.
type ClientSession struct {
+ legacy bool
requestBodyKey [16]byte
requestBodyIV [16]byte
responseBodyKey [16]byte
@@ -35,8 +38,8 @@
}
// NewClientSession creates a new ClientSession.
-func NewClientSession(ctx context.Context, behaviorSeed int64) *ClientSession {
- session := &ClientSession{}
+func NewClientSession(ctx context.Context, behaviorSeed int64, legacy bool) *ClientSession {
+ session := &ClientSession{legacy: legacy}
randomBytes := make([]byte, 33) // 16 + 16 + 1
common.Must2(rand.Read(randomBytes))
@@ -44,10 +47,15 @@
copy(session.requestBodyIV[:], randomBytes[16:32])
session.responseHeader = randomBytes[32]
- BodyKey := sha256.Sum256(session.requestBodyKey[:])
- copy(session.responseBodyKey[:], BodyKey[:16])
- BodyIV := sha256.Sum256(session.requestBodyIV[:])
- copy(session.responseBodyIV[:], BodyIV[:16])
+ if legacy {
+ session.responseBodyKey = md5.Sum(session.requestBodyKey[:])
+ session.responseBodyIV = md5.Sum(session.requestBodyIV[:])
+ } else {
+ BodyKey := sha256.Sum256(session.requestBodyKey[:])
+ copy(session.responseBodyKey[:], BodyKey[:16])
+ BodyIV := sha256.Sum256(session.requestBodyIV[:])
+ copy(session.responseBodyIV[:], BodyIV[:16])
+ }
{
var err error
session.readDrainer, err = drain.NewBehaviorSeedLimitedDrainer(behaviorSeed, 18, 3266, 64)
@@ -62,6 +70,15 @@
func (c *ClientSession) EncodeRequestHeader(header *protocol.RequestHeader, writer io.Writer) error {
account := header.User.Account.(*vmess.MemoryAccount)
+ var timestamp [8]byte
+ if c.legacy {
+ binary.BigEndian.PutUint64(timestamp[:], uint64(protocol.NowTime()))
+ auth := hmac.New(md5.New, account.AnyValidID().Bytes())
+ auth.Write(timestamp[:])
+ if _, err := writer.Write(auth.Sum(nil)); err != nil {
+ return err
+ }
+ }
buffer := buf.New()
defer buffer.Release()
@@ -93,6 +110,17 @@
fnv1a.Sum(hashBytes[:0])
}
+ if c.legacy {
+ iv := md5.New()
+ for i := 0; i < 4; i++ {
+ iv.Write(timestamp[:])
+ }
+ stream := crypto.NewAesEncryptionStream(account.ID.CmdKey(), iv.Sum(nil))
+ stream.XORKeyStream(buffer.Bytes(), buffer.Bytes())
+ _, err := writer.Write(buffer.Bytes())
+ return err
+ }
+
var fixedLengthCmdKey [16]byte
copy(fixedLengthCmdKey[:], account.ID.CmdKey())
vmessout := vmessaead.SealVMessAEADHeader(fixedLengthCmdKey, buffer.Bytes())
@@ -163,46 +191,51 @@
}
func (c *ClientSession) DecodeResponseHeader(reader io.Reader) (*protocol.ResponseHeader, error) {
- aeadResponseHeaderLengthEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderLenKey)
- aeadResponseHeaderLengthEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderLenIV)[:12]
-
- aeadResponseHeaderLengthEncryptionAEAD := crypto.NewAesGcm(aeadResponseHeaderLengthEncryptionKey)
-
- var aeadEncryptedResponseHeaderLength [18]byte
- var decryptedResponseHeaderLength int
- var decryptedResponseHeaderLengthBinaryDeserializeBuffer uint16
-
- if n, err := io.ReadFull(reader, aeadEncryptedResponseHeaderLength[:]); err != nil {
- c.readDrainer.AcknowledgeReceive(n)
- return nil, drain.WithError(c.readDrainer, reader, errors.New("Unable to Read Header Len").Base(err))
- } else { // nolint: golint
- c.readDrainer.AcknowledgeReceive(n)
- }
- if decryptedResponseHeaderLengthBinaryBuffer, err := aeadResponseHeaderLengthEncryptionAEAD.Open(nil, aeadResponseHeaderLengthEncryptionIV, aeadEncryptedResponseHeaderLength[:], nil); err != nil {
- return nil, drain.WithError(c.readDrainer, reader, errors.New("Failed To Decrypt Length").Base(err))
- } else { // nolint: golint
- common.Must(binary.Read(bytes.NewReader(decryptedResponseHeaderLengthBinaryBuffer), binary.BigEndian, &decryptedResponseHeaderLengthBinaryDeserializeBuffer))
- decryptedResponseHeaderLength = int(decryptedResponseHeaderLengthBinaryDeserializeBuffer)
- }
-
- aeadResponseHeaderPayloadEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadKey)
- aeadResponseHeaderPayloadEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadIV)[:12]
-
- aeadResponseHeaderPayloadEncryptionAEAD := crypto.NewAesGcm(aeadResponseHeaderPayloadEncryptionKey)
-
- encryptedResponseHeaderBuffer := make([]byte, decryptedResponseHeaderLength+16)
-
- if n, err := io.ReadFull(reader, encryptedResponseHeaderBuffer); err != nil {
- c.readDrainer.AcknowledgeReceive(n)
- return nil, drain.WithError(c.readDrainer, reader, errors.New("Unable to Read Header Data").Base(err))
- } else { // nolint: golint
- c.readDrainer.AcknowledgeReceive(n)
- }
-
- if decryptedResponseHeaderBuffer, err := aeadResponseHeaderPayloadEncryptionAEAD.Open(nil, aeadResponseHeaderPayloadEncryptionIV, encryptedResponseHeaderBuffer, nil); err != nil {
- return nil, drain.WithError(c.readDrainer, reader, errors.New("Failed To Decrypt Payload").Base(err))
- } else { // nolint: golint
- c.responseReader = bytes.NewReader(decryptedResponseHeaderBuffer)
+ if c.legacy {
+ stream := crypto.NewAesDecryptionStream(c.responseBodyKey[:], c.responseBodyIV[:])
+ c.responseReader = crypto.NewCryptionReader(stream, reader)
+ } else {
+ aeadResponseHeaderLengthEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderLenKey)
+ aeadResponseHeaderLengthEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderLenIV)[:12]
+
+ aeadResponseHeaderLengthEncryptionAEAD := crypto.NewAesGcm(aeadResponseHeaderLengthEncryptionKey)
+
+ var aeadEncryptedResponseHeaderLength [18]byte
+ var decryptedResponseHeaderLength int
+ var decryptedResponseHeaderLengthBinaryDeserializeBuffer uint16
+
+ if n, err := io.ReadFull(reader, aeadEncryptedResponseHeaderLength[:]); err != nil {
+ c.readDrainer.AcknowledgeReceive(n)
+ return nil, drain.WithError(c.readDrainer, reader, errors.New("Unable to Read Header Len").Base(err))
+ } else { // nolint: golint
+ c.readDrainer.AcknowledgeReceive(n)
+ }
+ if decryptedResponseHeaderLengthBinaryBuffer, err := aeadResponseHeaderLengthEncryptionAEAD.Open(nil, aeadResponseHeaderLengthEncryptionIV, aeadEncryptedResponseHeaderLength[:], nil); err != nil {
+ return nil, drain.WithError(c.readDrainer, reader, errors.New("Failed To Decrypt Length").Base(err))
+ } else { // nolint: golint
+ common.Must(binary.Read(bytes.NewReader(decryptedResponseHeaderLengthBinaryBuffer), binary.BigEndian, &decryptedResponseHeaderLengthBinaryDeserializeBuffer))
+ decryptedResponseHeaderLength = int(decryptedResponseHeaderLengthBinaryDeserializeBuffer)
+ }
+
+ aeadResponseHeaderPayloadEncryptionKey := vmessaead.KDF16(c.responseBodyKey[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadKey)
+ aeadResponseHeaderPayloadEncryptionIV := vmessaead.KDF(c.responseBodyIV[:], vmessaead.KDFSaltConstAEADRespHeaderPayloadIV)[:12]
+
+ aeadResponseHeaderPayloadEncryptionAEAD := crypto.NewAesGcm(aeadResponseHeaderPayloadEncryptionKey)
+
+ encryptedResponseHeaderBuffer := make([]byte, decryptedResponseHeaderLength+16)
+
+ if n, err := io.ReadFull(reader, encryptedResponseHeaderBuffer); err != nil {
+ c.readDrainer.AcknowledgeReceive(n)
+ return nil, drain.WithError(c.readDrainer, reader, errors.New("Unable to Read Header Data").Base(err))
+ } else { // nolint: golint
+ c.readDrainer.AcknowledgeReceive(n)
+ }
+
+ if decryptedResponseHeaderBuffer, err := aeadResponseHeaderPayloadEncryptionAEAD.Open(nil, aeadResponseHeaderPayloadEncryptionIV, encryptedResponseHeaderBuffer, nil); err != nil {
+ return nil, drain.WithError(c.readDrainer, reader, errors.New("Failed To Decrypt Payload").Base(err))
+ } else { // nolint: golint
+ c.responseReader = bytes.NewReader(decryptedResponseHeaderBuffer)
+ }
}
buffer := buf.StackNew()
--- a/proxy/vmess/encoding/encoding_test.go
+++ b/proxy/vmess/encoding/encoding_test.go
@@ -41,7 +41,7 @@
}
buffer := buf.New()
- client := NewClientSession(context.TODO(), 0)
+ client := NewClientSession(context.TODO(), 0, false)
common.Must(client.EncodeRequestHeader(expectedRequest, buffer))
buffer2 := buf.New()
@@ -90,7 +90,7 @@
}
buffer := buf.New()
- client := NewClientSession(context.TODO(), 0)
+ client := NewClientSession(context.TODO(), 0, false)
common.Must(client.EncodeRequestHeader(expectedRequest, buffer))
buffer2 := buf.New()
@@ -130,7 +130,7 @@
}
buffer := buf.New()
- client := NewClientSession(context.TODO(), 0)
+ client := NewClientSession(context.TODO(), 0, false)
common.Must(client.EncodeRequestHeader(expectedRequest, buffer))
buffer2 := buf.New()
--- a/proxy/vmess/encoding/legacy_test.go
+++ b/proxy/vmess/encoding/legacy_test.go
@@ -0,0 +1,159 @@
+package encoding
+
+import (
+ "bytes"
+ "context"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "crypto/md5"
+ "encoding/binary"
+ "fmt"
+ "hash/fnv"
+ "io"
+ "testing"
+ "time"
+
+ "github.com/xtls/xray-core/common/buf"
+ "github.com/xtls/xray-core/common/net"
+ "github.com/xtls/xray-core/common/protocol"
+ "github.com/xtls/xray-core/proxy/vmess"
+)
+
+func legacyRequest(t *testing.T, security protocol.SecurityType, command protocol.RequestCommand) (*ClientSession, *protocol.RequestHeader, []byte) {
+ t.Helper()
+ a, err := (&vmess.Account{Id: "e641f5ad-9397-41e3-bf1a-e8740dfed019", AlterId: 1}).AsAccount()
+ if err != nil {
+ t.Fatal(err)
+ }
+ req := &protocol.RequestHeader{Version: 1, User: &protocol.MemoryUser{Account: a}, Command: command, Address: net.DomainAddress("example.com"), Port: 443, Security: security, Option: protocol.RequestOptionChunkStream | protocol.RequestOptionChunkMasking | protocol.RequestOptionGlobalPadding}
+ c := NewClientSession(context.Background(), 0, true)
+ var wire bytes.Buffer
+ if err := c.EncodeRequestHeader(req, &wire); err != nil {
+ t.Fatal(err)
+ }
+ raw := wire.Bytes()
+ // Independently recover the timestamp from the legacy HMAC-MD5 authenticator.
+ var stamp [8]byte
+ found := false
+ for ts := time.Now().Unix() - 35; ts <= time.Now().Unix()+35; ts++ {
+ binary.BigEndian.PutUint64(stamp[:], uint64(ts))
+ auth := hmac.New(md5.New, a.(*vmess.MemoryAccount).AlterIDs[0].Bytes())
+ auth.Write(stamp[:])
+ if hmac.Equal(auth.Sum(nil), raw[:16]) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Fatal("legacy authentication ID or timestamp mismatch")
+ }
+ iv := md5.Sum(bytes.Repeat(stamp[:], 4))
+ block, err := aes.NewCipher(a.(*vmess.MemoryAccount).ID.CmdKey())
+ if err != nil {
+ t.Fatal(err)
+ }
+ header := append([]byte(nil), raw[16:]...)
+ cipher.NewCFBDecrypter(block, iv[:]).XORKeyStream(header, header)
+ h := fnv.New32a()
+ h.Write(header[:len(header)-4])
+ if binary.BigEndian.Uint32(header[len(header)-4:]) != h.Sum32() {
+ t.Fatal("invalid request header checksum")
+ }
+ if header[0] != 1 || header[37] != byte(command) || header[35]&15 != byte(security) {
+ t.Fatal("invalid request header fields")
+ }
+ return c, req, header
+}
+
+func TestLegacyVMessTCPAndUDP(t *testing.T) {
+ for _, security := range []protocol.SecurityType{protocol.SecurityType_AES128_GCM, protocol.SecurityType_CHACHA20_POLY1305} {
+ for _, command := range []protocol.RequestCommand{protocol.RequestCommandTCP, protocol.RequestCommandUDP} {
+ t.Run(fmt.Sprintf("%v/%v", security, command), func(t *testing.T) {
+ c, req, header := legacyRequest(t, security, command)
+ server := &ServerSession{}
+ copy(server.requestBodyIV[:], header[1:17])
+ copy(server.requestBodyKey[:], header[17:33])
+ server.responseBodyKey = md5.Sum(header[17:33])
+ server.responseBodyIV = md5.Sum(header[1:17])
+ var upload, download bytes.Buffer
+ writer, err := c.EncodeRequestBody(req, &upload)
+ if err != nil {
+ t.Fatal(err)
+ }
+ payload := bytes.Repeat([]byte("legacy vmess packet "), 40)
+ if err = writer.WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(payload)}); err != nil {
+ t.Fatal(err)
+ }
+ reader, err := server.DecodeRequestBody(req, &upload)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assertPacket(t, reader, payload)
+ response := []byte{header[33], 0, 0, 0}
+ block, err := aes.NewCipher(server.responseBodyKey[:])
+ if err != nil {
+ t.Fatal(err)
+ }
+ cipher.NewCFBEncrypter(block, server.responseBodyIV[:]).XORKeyStream(response, response)
+ download.Write(response)
+ writer, err = server.EncodeResponseBody(req, &download)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err = writer.WriteMultiBuffer(buf.MultiBuffer{buf.FromBytes(payload)}); err != nil {
+ t.Fatal(err)
+ }
+ if _, err = c.DecodeResponseHeader(&download); err != nil {
+ t.Fatal(err)
+ }
+ reader, err = c.DecodeResponseBody(req, &download)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assertPacket(t, reader, payload)
+ })
+ }
+ }
+}
+
+func assertPacket(t *testing.T, r buf.Reader, want []byte) {
+ t.Helper()
+ b, err := r.ReadMultiBuffer()
+ defer buf.ReleaseMulti(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var got []byte
+ for _, part := range b {
+ got = append(got, part.Bytes()...)
+ }
+ if !bytes.Equal(got, want) {
+ t.Fatal("payload mismatch")
+ }
+}
+
+func TestLegacyResponseRejectsInvalidHeader(t *testing.T) {
+ for _, truncated := range []bool{false, true} {
+ c, _, header := legacyRequest(t, protocol.SecurityType_AES128_GCM, protocol.RequestCommandTCP)
+ response := []byte{header[33] ^ 1, 0, 0, 0}
+ block, _ := aes.NewCipher(c.responseBodyKey[:])
+ cipher.NewCFBEncrypter(block, c.responseBodyIV[:]).XORKeyStream(response, response)
+ if truncated {
+ response = response[:2]
+ }
+ if _, err := c.DecodeResponseHeader(bytes.NewReader(response)); err == nil {
+ t.Fatal("accepted invalid response")
+ }
+ }
+}
+
+type failedLegacyWriter struct{}
+
+func (failedLegacyWriter) Write([]byte) (int, error) { return 0, io.ErrClosedPipe }
+func TestLegacyRequestWriteError(t *testing.T) {
+ c, req, _ := legacyRequest(t, protocol.SecurityType_AES128_GCM, protocol.RequestCommandTCP)
+ if err := c.EncodeRequestHeader(req, failedLegacyWriter{}); err != io.ErrClosedPipe {
+ t.Fatalf("got %v", err)
+ }
+}
--- a/proxy/vmess/outbound/outbound.go
+++ b/proxy/vmess/outbound/outbound.go
@@ -132,7 +132,7 @@
newCtx, newCancel = context.WithCancel(context.Background())
}
- session := encoding.NewClientSession(ctx, int64(behaviorSeed))
+ session := encoding.NewClientSession(ctx, int64(behaviorSeed), len(account.AlterIDs) != 0)
sessionPolicy := h.policyManager.ForLevel(request.User.Level)
ctx, cancel := context.WithCancel(ctx)
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,515 @@
From: coolsnowwolf <coolsnowwolf@gmail.com>
Subject: [PATCH] build: add AN7581-only EIP93 hybrid AES-GCM overlay
Keep the shared Go toolchain unchanged. Select the AF_ALG backend only on
AN7581 without ARM AES instructions, with per-key verification and software
fallback. AN7583 remains on the normal path pending separate measurements.
Signed-off-by: coolsnowwolf <coolsnowwolf@gmail.com>
--- a/tools/eip93/eip93_linux_arm64.go
+++ b/tools/eip93/eip93_linux_arm64.go
@@ -0,0 +1,294 @@
+// Copyright 2026 coolsnowwolf <coolsnowwolf@gmail.com>
+// SPDX-License-Identifier: BSD-3-Clause
+//go:build linux && arm64 && !purego
+
+package gcm
+
+import (
+ "bytes"
+ "crypto/internal/fips140"
+ "crypto/internal/fips140/aes"
+ "crypto/internal/fips140deps/cpu"
+ "encoding/binary"
+ "os"
+ "runtime"
+ "sync"
+ "sync/atomic"
+ "syscall"
+ "unsafe"
+)
+
+const eip93SOL = 279
+const eip93Min = 4096
+const eip93Max = 32768
+const eip93MaxSockets = 128
+
+var eip93Detected sync.Once
+var eip93Allowed bool
+var eip93Sockets atomic.Int32
+var eip93Notice sync.Once
+
+func eip93Compatible(data []byte) bool {
+ if len(data) == 0 || data[len(data)-1] != 0 {
+ return false
+ }
+ found := false
+ for _, s := range bytes.Split(data, []byte{0}) {
+ switch string(s) {
+ case "airoha,an7583", "airoha,en7583":
+ return false
+ case "airoha,an7581", "airoha,en7581":
+ found = true
+ }
+ }
+ return found
+}
+
+func eip93Enabled() bool {
+ eip93Detected.Do(func() {
+ if fips140.Enabled || cpu.ARM64HasAES || os.Getenv("XRAY_EIP93") == "off" {
+ return
+ }
+ for _, path := range []string{"/sys/firmware/devicetree/base/compatible", "/proc/device-tree/compatible"} {
+ b, err := os.ReadFile(path)
+ if err == nil {
+ eip93Allowed = eip93Compatible(b)
+ return
+ }
+ }
+ })
+ return eip93Allowed
+}
+
+type eip93State struct {
+ mu sync.Mutex
+ key [32]byte
+ keyLen int
+ fd int
+ disabled bool
+ input, output []byte
+}
+
+func newEIP93State(g *GCM) *eip93State {
+ if g.nonceSize != 12 || g.tagSize != 16 || !eip93Enabled() {
+ return nil
+ }
+ // Only the generic ARM64 schedule is used: its first Nk big-endian words
+ // are the original AES key. Never interpret an instruction-specific schedule.
+ schedule := aes.EncryptionKeySchedule(&g.cipher)
+ keyLen := (len(schedule)/4 - 7) * 4
+ if keyLen != 16 && keyLen != 24 && keyLen != 32 {
+ return nil
+ }
+ s := &eip93State{keyLen: keyLen, fd: -1}
+ for i := 0; i < keyLen/4; i++ {
+ binary.BigEndian.PutUint32(s.key[i*4:], schedule[i])
+ }
+ runtime.SetFinalizer(s, func(s *eip93State) { s.close(); clear(s.key[:]) })
+ return s
+}
+
+func (s *eip93State) close() {
+ if s.fd >= 0 {
+ syscall.Close(s.fd)
+ s.fd = -1
+ eip93Sockets.Add(-1)
+ }
+ clear(s.input)
+ clear(s.output)
+}
+
+func eip93Cmsg(kind int32, data []byte) []byte {
+ b := make([]byte, syscall.CmsgSpace(len(data)))
+ h := (*syscall.Cmsghdr)(unsafe.Pointer(&b[0]))
+ h.Level = eip93SOL
+ h.Type = kind
+ h.SetLen(syscall.CmsgLen(len(data)))
+ copy(b[syscall.CmsgLen(0):], data)
+ return b
+}
+func eip93U32(v uint32) []byte { b := make([]byte, 4); binary.LittleEndian.PutUint32(b, v); return b }
+
+// Linux 5.4 uses the CryptoAPI GHASH driver; Linux 7.3 uses the library.
+func eip93Bind(bind func(string) syscall.Errno) syscall.Errno {
+ var errno syscall.Errno
+ for _, name := range []string{
+ "gcm_base(ctr(aes-eip93),ghash-generic)",
+ "gcm_base(ctr(aes-eip93),ghash-lib)",
+ } {
+ errno = bind(name)
+ if errno == 0 {
+ return 0
+ }
+ }
+ return errno
+}
+
+func (s *eip93State) connect() error {
+ if eip93Sockets.Add(1) > eip93MaxSockets {
+ eip93Sockets.Add(-1)
+ return syscall.EMFILE
+ }
+ success := false
+ defer func() {
+ if !success {
+ eip93Sockets.Add(-1)
+ }
+ }()
+ fd, err := syscall.Socket(38, syscall.SOCK_SEQPACKET|syscall.SOCK_CLOEXEC, 0)
+ if err != nil {
+ return err
+ }
+ defer syscall.Close(fd)
+ var sa [88]byte
+ binary.LittleEndian.PutUint16(sa[:], 38)
+ copy(sa[2:16], "aead")
+ // Try both exact EIP93 compositions even when a kernel policy or driver
+ // rejects the first one. Never substitute a generic software CTR driver.
+ errno := eip93Bind(func(name string) syscall.Errno {
+ clear(sa[24:])
+ copy(sa[24:], name)
+ _, _, errno := syscall.Syscall(syscall.SYS_BIND, uintptr(fd), uintptr(unsafe.Pointer(&sa[0])), uintptr(len(sa)))
+ return errno
+ })
+ if errno != 0 {
+ return errno
+ }
+ _, _, errno = syscall.Syscall6(syscall.SYS_SETSOCKOPT, uintptr(fd), eip93SOL, 1, uintptr(unsafe.Pointer(&s.key[0])), uintptr(s.keyLen), 0)
+ runtime.KeepAlive(s)
+ if errno != 0 {
+ return errno
+ }
+ _, _, errno = syscall.Syscall6(syscall.SYS_SETSOCKOPT, uintptr(fd), eip93SOL, 5, 0, 16, 0)
+ if errno != 0 {
+ return errno
+ }
+ op, _, errno := syscall.Syscall6(syscall.SYS_ACCEPT4, uintptr(fd), 0, 0, syscall.SOCK_CLOEXEC, 0, 0)
+ if errno != 0 {
+ return errno
+ }
+ for _, option := range []int{syscall.SO_RCVTIMEO, syscall.SO_SNDTIMEO} {
+ if err = syscall.SetsockoptTimeval(int(op), syscall.SOL_SOCKET, option, &syscall.Timeval{Sec: 1}); err != nil {
+ syscall.Close(int(op))
+ return err
+ }
+ }
+ s.fd = int(op)
+ success = true
+ clear(s.key[:])
+ return nil
+}
+
+// exchange never exposes plaintext until the kernel has authenticated it.
+// It also never modifies the caller's buffers before a complete operation,
+// so a syscall failure can safely fall back to the original implementation.
+func (s *eip93State) exchange(nonce, src, aad []byte, decrypt bool) ([]byte, error) {
+ inLen := len(aad) + len(src)
+ outLen := inLen + 16
+ if decrypt {
+ outLen = inLen - 16
+ }
+ if cap(s.input) < inLen {
+ s.input = make([]byte, inLen)
+ } else {
+ s.input = s.input[:inLen]
+ }
+ if cap(s.output) < outLen {
+ s.output = make([]byte, outLen)
+ } else {
+ s.output = s.output[:outLen]
+ }
+ copy(s.input, aad)
+ copy(s.input[len(aad):], src)
+ op := uint32(1)
+ if decrypt {
+ op = 0
+ }
+ ctl := eip93Cmsg(3, eip93U32(op))
+ iv := append(eip93U32(12), nonce...)
+ ctl = append(ctl, eip93Cmsg(2, iv)...)
+ ctl = append(ctl, eip93Cmsg(4, eip93U32(uint32(len(aad))))...)
+ n, err := syscall.SendmsgN(s.fd, s.input, ctl, nil, 0)
+ if err != nil {
+ return nil, err
+ }
+ if n != inLen {
+ return nil, syscall.EIO
+ }
+ n, err = syscall.Read(s.fd, s.output)
+ if err != nil {
+ return nil, err
+ }
+ if n != outLen {
+ return nil, syscall.EIO
+ }
+ return s.output[len(aad):], nil
+}
+
+func (s *eip93State) selftest(g *GCM) bool {
+ var nonce [12]byte
+ n, _, errno := syscall.Syscall(syscall.SYS_GETRANDOM, uintptr(unsafe.Pointer(&nonce[0])), uintptr(len(nonce)), 0)
+ if errno != 0 || n != uintptr(len(nonce)) {
+ return false
+ }
+ aad := []byte("EIP93 selftest")
+ plain := make([]byte, 257)
+ for i := range plain {
+ plain[i] = byte(i * 7)
+ }
+ expected := make([]byte, len(plain)+16)
+ sealGeneric(expected, g, nonce[:], plain, aad)
+ got, err := s.exchange(nonce[:], plain, aad, false)
+ if err != nil || !bytes.Equal(got, expected) {
+ return false
+ }
+ got, err = s.exchange(nonce[:], expected, aad, true)
+ if err != nil || !bytes.Equal(got, plain) {
+ return false
+ }
+ expected[len(expected)-1] ^= 1
+ _, err = s.exchange(nonce[:], expected, aad, true)
+ return err == syscall.EBADMSG
+}
+
+// Return handled=false for unsupported sizes and operational failures.
+// Authentication failures are handled=true, valid=false, never successes.
+func (s *eip93State) crypt(g *GCM, out, nonce, src, aad []byte, decrypt bool) (handled, valid bool) {
+ n := len(src)
+ if decrypt {
+ n -= 16
+ }
+ if n < eip93Min || n > eip93Max || len(aad) > 256 || len(nonce) != 12 {
+ return false, false
+ }
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ defer runtime.KeepAlive(s)
+ if s.disabled {
+ return false, false
+ }
+ if s.fd < 0 {
+ if s.connect() != nil || !s.selftest(g) {
+ s.close()
+ clear(s.key[:])
+ s.disabled = true
+ return false, false
+ }
+ eip93Notice.Do(func() { println("Xray EIP93: AN7581 AES-GCM hybrid backend enabled (AF_ALG CTR + CPU GHASH)") })
+ }
+ result, err := s.exchange(nonce, src, aad, decrypt)
+ if decrypt && err == syscall.EBADMSG {
+ clear(s.input)
+ clear(s.output)
+ return true, false
+ }
+ if err != nil {
+ s.close()
+ s.disabled = true
+ return false, false
+ }
+ copy(out, result)
+ clear(s.input)
+ clear(s.output)
+ return true, true
+}
--- a/tools/eip93/eip93_linux_arm64_test.go
+++ b/tools/eip93/eip93_linux_arm64_test.go
@@ -0,0 +1,153 @@
+// Copyright 2026 coolsnowwolf <coolsnowwolf@gmail.com>
+// SPDX-License-Identifier: BSD-3-Clause
+//go:build linux && arm64 && !purego
+
+package gcm
+
+import (
+ "bytes"
+ "crypto/internal/fips140/aes"
+ "sync"
+ "syscall"
+ "testing"
+)
+
+func TestEIP93DTGate(t *testing.T) {
+ for _, tc := range []struct {
+ dt string
+ want bool
+ }{
+ {"nokia,xg-040g-md-ubi\x00airoha,an7581\x00", true},
+ {"airoha,en7581\x00", true}, {"airoha,an7583\x00", false},
+ {"airoha,an7581\x00airoha,en7583\x00", false},
+ {"airoha,an7581", false}, {"", false}, {"vendor,an7581\x00", false}, {"airoha,an75810\x00", false},
+ } {
+ if eip93Compatible([]byte(tc.dt)) != tc.want {
+ t.Fatalf("gate %q", tc.dt)
+ }
+ }
+}
+
+func TestEIP93Hardware(t *testing.T) {
+ if !eip93Enabled() {
+ t.Skip("requires AN7581 with EIP93 backend enabled")
+ }
+ for _, keyLen := range []int{16, 24, 32} {
+ key := make([]byte, keyLen)
+ for i := range key {
+ key[i] = byte(i + 1)
+ }
+ block, err := aes.New(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ g, err := New(block, 12, 16)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if g.eip93 == nil {
+ t.Fatal("backend not selected")
+ }
+ for _, n := range []int{0, 1, 15, 16, 17, 4095, 4096, 4097, 16384, 16385, 32768, 32769} {
+ plain := make([]byte, n)
+ for i := range plain {
+ plain[i] = byte(i * 7)
+ }
+ nonce := make([]byte, 12)
+ nonce[0] = byte(n)
+ nonce[1] = byte(n >> 8)
+ aad := []byte("authentication data")
+ want := make([]byte, n+16)
+ sealGeneric(want, g, nonce, plain, aad)
+ got := g.Seal(nil, nonce, plain, aad)
+ if !bytes.Equal(got, want) {
+ t.Fatalf("seal key=%d n=%d", keyLen, n)
+ }
+ buf := append([]byte(nil), got...)
+ out, err := g.Open(buf[:0], nonce, buf, aad)
+ if err != nil || !bytes.Equal(out, plain) {
+ t.Fatalf("inplace open key=%d n=%d err=%v", keyLen, n, err)
+ }
+ buf = make([]byte, n, n+16)
+ copy(buf, plain)
+ got = g.Seal(buf[:0], nonce, buf, aad)
+ if !bytes.Equal(got, want) {
+ t.Fatalf("inplace seal key=%d n=%d", keyLen, n)
+ }
+ got[len(got)-1] ^= 1
+ out, err = g.Open(nil, nonce, got, aad)
+ if err == nil || out != nil {
+ t.Fatal("corrupt tag accepted")
+ }
+ }
+ if g.eip93.fd < 0 || g.eip93.disabled {
+ t.Fatal("hardware failed or fell back")
+ }
+ var wg sync.WaitGroup
+ for worker := 0; worker < 8; worker++ {
+ wg.Add(1)
+ go func(id int) {
+ defer wg.Done()
+ for i := 0; i < 20; i++ {
+ nonce := make([]byte, 12)
+ nonce[0] = byte(id + 64)
+ nonce[1] = byte(i)
+ plain := bytes.Repeat([]byte{byte(id)}, 8192)
+ got := g.Seal(nil, nonce, plain, nil)
+ out, err := g.Open(nil, nonce, got, nil)
+ if err != nil || !bytes.Equal(out, plain) {
+ t.Error("concurrent roundtrip")
+ }
+ }
+ }(worker)
+ }
+ wg.Wait()
+ // A closed operation fd must cause a safe software fallback, without
+ // returning partial plaintext or leaking the global socket reservation.
+ syscall.Close(g.eip93.fd)
+ plain := make([]byte, 8192)
+ nonce := make([]byte, 12)
+ want := make([]byte, len(plain)+16)
+ sealGeneric(want, g, nonce, plain, nil)
+ if got := g.Seal(nil, nonce, plain, nil); !bytes.Equal(got, want) || !g.eip93.disabled {
+ t.Fatal("closed-fd fallback")
+ }
+ for _, nt := range [][2]int{{8, 16}, {12, 12}} {
+ other, err := New(block, nt[0], nt[1])
+ if err != nil || other.eip93 != nil {
+ t.Fatal("nonstandard GCM must use software")
+ }
+ }
+ }
+ if n := eip93Sockets.Load(); n != 0 {
+ t.Fatalf("socket reservations leaked: %d", n)
+ }
+}
+
+func TestEIP93BindFallback(t *testing.T) {
+ for _, first := range []syscall.Errno{0, syscall.ENOENT, syscall.EINVAL, syscall.ENODEV, syscall.EACCES, syscall.EPERM} {
+ for _, second := range []syscall.Errno{0, syscall.ENOENT} {
+ calls := 0
+ err := eip93Bind(func(name string) syscall.Errno {
+ calls++
+ if calls == 1 {
+ if name != "gcm_base(ctr(aes-eip93),ghash-generic)" {
+ t.Fatal(name)
+ }
+ return first
+ }
+ if name != "gcm_base(ctr(aes-eip93),ghash-lib)" {
+ t.Fatal(name)
+ }
+ return second
+ })
+ want, count := second, 2
+ if first == 0 {
+ want, count = 0, 1
+ }
+ if err != want || calls != count {
+ t.Fatalf("first=%v second=%v: err=%v calls=%d", first, second, err, calls)
+ }
+ }
+ }
+}
--- a/tools/eip93/eip93_stub.go
+++ b/tools/eip93/eip93_stub.go
@@ -0,0 +1,12 @@
+// Copyright 2026 coolsnowwolf <coolsnowwolf@gmail.com>
+// SPDX-License-Identifier: BSD-3-Clause
+//go:build !linux || !arm64 || purego
+
+package gcm
+
+type eip93State struct{}
+
+func newEIP93State(g *GCM) *eip93State { return nil }
+func (*eip93State) crypt(g *GCM, out, nonce, src, aad []byte, decrypt bool) (bool, bool) {
+ return false, false
+}
--- a/tools/eip93/overlay.py
+++ b/tools/eip93/overlay.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+"""Generate an Xray-only Go overlay; never modify the shared Go toolchain."""
+import argparse
+import json
+from pathlib import Path
+import subprocess
+
+parser=argparse.ArgumentParser()
+parser.add_argument('--go',required=True)
+parser.add_argument('--out',required=True)
+args=parser.parse_args()
+root=Path(subprocess.check_output([args.go,'env','GOROOT'],text=True).strip()).resolve()
+source=root/'src/crypto/internal/fips140/aes/gcm'
+out=Path(args.out).resolve();out.mkdir(parents=True,exist_ok=True)
+replacements={}
+def change(name,edits):
+ text=(source/name).read_text()
+ for old,new in edits:
+ if text.count(old)!=1:
+ raise SystemExit('EIP93 overlay: unsupported Go source in '+name+'; review before enabling acceleration')
+ text=text.replace(old,new,1)
+ dst=out/name;dst.write_text(text)
+ replacements[str(source/name)]=str(dst)
+change('gcm.go',[('\tgcmPlatformData\n','\tgcmPlatformData\n\teip93 *eip93State\n')])
+change('gcm_asm.go',[
+ ('func initGCM(g *GCM) {\n\tif !supportsAESGCM {\n\t\treturn',
+ 'func initGCM(g *GCM) {\n\tif !supportsAESGCM {\n\t\tg.eip93 = newEIP93State(g)\n\t\treturn'),
+ ('\t\tsealGeneric(out, g, nonce, plaintext, data)',
+ '\t\tif g.eip93 != nil {\n\t\t\tif handled, _ := g.eip93.crypt(g, out, nonce, plaintext, data, false); handled { return }\n\t\t}\n\t\tsealGeneric(out, g, nonce, plaintext, data)'),
+ ('\t\treturn openGeneric(out, g, nonce, ciphertext, data)',
+ '\t\tif g.eip93 != nil {\n\t\t\tif handled, valid := g.eip93.crypt(g, out, nonce, ciphertext, data, true); handled {\n\t\t\t\tif !valid { return errOpen }; return nil\n\t\t\t}\n\t\t}\n\t\treturn openGeneric(out, g, nonce, ciphertext, data)')])
+for name in ['eip93_linux_arm64.go','eip93_stub.go','eip93_linux_arm64_test.go']:
+ dst=out/name;dst.write_text((Path(__file__).parent/name).read_text())
+ replacements[str(source/name)]=str(dst)
+(out/'overlay.json').write_text(json.dumps({'Replace':replacements},indent=2)+'\n')