diff --git a/frontend/src/models/inbound.js b/frontend/src/models/inbound.js
index 24a55f85..4ffa782e 100644
--- a/frontend/src/models/inbound.js
+++ b/frontend/src/models/inbound.js
@@ -2879,7 +2879,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
constructor(protocol,
method = SSMethods.BLAKE3_AES_256_GCM,
password = RandomUtil.randomShadowsocksPassword(),
- network = 'tcp,udp',
+ network = 'tcp',
shadowsockses = [],
ivCheck = false,
) {
diff --git a/frontend/src/pages/inbounds/InboundFormModal.vue b/frontend/src/pages/inbounds/InboundFormModal.vue
index e0e8f938..791522fa 100644
--- a/frontend/src/pages/inbounds/InboundFormModal.vue
+++ b/frontend/src/pages/inbounds/InboundFormModal.vue
@@ -34,22 +34,17 @@ import JsonEditor from '@/components/JsonEditor.vue';
import { useNodeList } from '@/composables/useNodeList.js';
const { t } = useI18n();
-
-// Node selector — Phase 1 multi-node deployment. Shows all enabled
-// nodes regardless of online state so the form is usable while a node
-// is briefly offline; the backend's fail-fast path will surface the
-// real error when the user submits.
const { nodes: availableNodes } = useNodeList();
const selectableNodes = computed(() => (availableNodes.value || []).filter((n) => n.enable));
-
-// Phase 5f-iii-b: structured per-protocol/per-transport forms instead
-// of raw JSON textareas. Edits a deeply-reactive Inbound + DBInbound
-// pair so the existing model helpers (.toString(), .canEnableTls(),
-// genAllLinks(), addPeer(), etc.) keep working unchanged. The
-// "Advanced" tab still exposes the full streamSettings JSON for
-// transport variants (KCP/XHTTP/sockopt/finalmask) we don't yet have
-// dedicated UI for.
-
+const NODE_ELIGIBLE_PROTOCOLS = new Set([
+ Protocols.VLESS,
+ Protocols.VMESS,
+ Protocols.TROJAN,
+ Protocols.SHADOWSOCKS,
+ Protocols.HYSTERIA,
+ Protocols.WIREGUARD,
+]);
+const isNodeEligible = computed(() => NODE_ELIGIBLE_PROTOCOLS.has(inbound.value?.protocol));
const props = defineProps({
open: { type: Boolean, default: false },
mode: { type: String, default: 'add', validator: (v) => ['add', 'edit'].includes(v) },
@@ -72,8 +67,6 @@ const advancedSniffingText = ref('');
const advancedSettingsText = ref('');
const activeTabKey = ref('basic');
const advancedSectionKey = ref('all');
-// Cached default cert/key paths from /panel/setting/defaultSettings —
-// powers the "Set default cert" button on the TLS form.
const defaultCert = ref('');
const defaultKey = ref('');
@@ -334,6 +327,9 @@ function onProtocolChange(next) {
if (props.mode === 'edit' || !inbound.value) return;
inbound.value.protocol = next;
inbound.value.settings = Inbound.Settings.getSettings(next);
+ if (!NODE_ELIGIBLE_PROTOCOLS.has(next)) {
+ dbForm.value.nodeId = null;
+ }
primeAdvancedJson();
}
@@ -792,7 +788,7 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
-
+
{{ t('pages.inbounds.localPanel') }}
@@ -1164,8 +1160,7 @@ watch(() => inbound.value?.protocol, () => stampAdvancedTextFor('stream'));
-
+
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index b414e813..78c4d657 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -78,19 +78,14 @@ function injectBasePathPlugin() {
function bypassMigratedRoute(req) {
if (req.method !== 'GET') return undefined;
const url = req.url.split('?')[0];
+ const basePath = refreshBasePath();
- for (const [key, value] of Object.entries(BASE_MIGRATED_ROUTES)) {
- if (url === '/' + key) return value;
- }
+ if (url === basePath) return '/login.html';
- const m = url.match(/^\/[^/]+\/(.+)$/);
- if (m) {
- const stripped = m[1];
+ if (url.startsWith(basePath)) {
+ const stripped = url.slice(basePath.length);
if (stripped in BASE_MIGRATED_ROUTES) return BASE_MIGRATED_ROUTES[stripped];
}
-
- if (url === '/' || /^\/[^/]+\/$/.test(url)) return '/login.html';
-
return undefined;
}
diff --git a/sub/subController.go b/sub/subController.go
index 9c7414c5..2eeeefe7 100644
--- a/sub/subController.go
+++ b/sub/subController.go
@@ -15,6 +15,18 @@ import (
"github.com/gin-gonic/gin"
)
+// writeSubError translates a service-layer result into an HTTP response.
+// A nil error with no rows means the subId doesn't match anything (deleted
+// client, never-existed id) and becomes 404. A real error becomes 500. No
+// body — VPN clients only look at the status.
+func writeSubError(c *gin.Context, err error) {
+ if err == nil {
+ c.Status(http.StatusNotFound)
+ return
+ }
+ c.Status(http.StatusInternalServerError)
+}
+
// SUBController handles HTTP requests for subscription links and JSON configurations.
type SUBController struct {
subTitle string
@@ -105,7 +117,7 @@ func (a *SUBController) subs(c *gin.Context) {
scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)
subs, lastOnline, traffic, err := a.subService.GetSubs(subId, host)
if err != nil || len(subs) == 0 {
- c.String(400, "Error!")
+ writeSubError(c, err)
} else {
result := ""
for _, sub := range subs {
@@ -240,7 +252,7 @@ func (a *SUBController) subJsons(c *gin.Context) {
scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c)
jsonSub, header, err := a.subJsonService.GetJson(subId, host)
if err != nil || len(jsonSub) == 0 {
- c.String(400, "Error!")
+ writeSubError(c, err)
} else {
profileUrl := a.subProfileUrl
if profileUrl == "" {
@@ -257,7 +269,7 @@ func (a *SUBController) subClashs(c *gin.Context) {
scheme, host, hostWithPort, _ := a.subService.ResolveRequest(c)
clashSub, header, err := a.subClashService.GetClash(subId, host)
if err != nil || len(clashSub) == 0 {
- c.String(400, "Error!")
+ writeSubError(c, err)
} else {
profileUrl := a.subProfileUrl
if profileUrl == "" {
diff --git a/sub/subService.go b/sub/subService.go
index afa9afca..c26a1ac1 100644
--- a/sub/subService.go
+++ b/sub/subService.go
@@ -70,7 +70,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
}
if len(inbounds) == 0 {
- return nil, 0, traffic, common.NewError("No inbounds found with ", subId)
+ return nil, 0, traffic, nil
}
s.datepicker, err = s.settingService.GetDatepicker()
@@ -535,8 +535,9 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin
return strings.Join(links, "\n")
}
- // No external proxy configured — fall back to the request host.
- link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, s.address, inbound.Port)
+ // No external proxy configured — use the inbound's resolved address so
+ // node-managed inbounds get the node's host instead of the central panel's.
+ link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, s.resolveInboundAddress(inbound), inbound.Port)
url, _ := url.Parse(link)
q := url.Query()
for k, v := range params {