diff --git a/frontend/src/pages/inbounds/form/useSecurityActions.ts b/frontend/src/pages/inbounds/form/useSecurityActions.ts index 53428093e..980fe6bc8 100644 --- a/frontend/src/pages/inbounds/form/useSecurityActions.ts +++ b/frontend/src/pages/inbounds/form/useSecurityActions.ts @@ -143,9 +143,14 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS messageApi.warning(t('pages.inbounds.form.pinFromRemoteNoSni')); return; } + // `xray tls ping` defaults to :443, but a self-hosted inbound rarely + // listens there. Append the inbound's own port (unless the SNI already + // carries one) so the ping reaches the actual TLS endpoint. + const port = form.getFieldValue('port') as number | undefined; + const target = /:\d+$/.test(server) || !port ? server : `${server}:${port}`; setSaving(true); try { - const msg = await HttpUtil.post('/panel/api/server/getRemoteCertHash', { server }); + const msg = await HttpUtil.post('/panel/api/server/getRemoteCertHash', { server: target }); if (!msg?.success) { messageApi.warning(msg?.msg || t('pages.inbounds.form.pinFromRemoteFailed')); return; diff --git a/internal/web/service/server.go b/internal/web/service/server.go index b1a05393d..878ab985f 100644 --- a/internal/web/service/server.go +++ b/internal/web/service/server.go @@ -1896,6 +1896,15 @@ func (s *ServerService) GetRemoteCertHash(server string) ([]string, error) { } } if len(leaves) == 0 { + // Surface why the ping produced no cert (dial refused, timeout, …) + // instead of the bare "not found" — the inbound is usually just not + // listening for TLS on the pinged port. + for _, line := range strings.Split(out.String(), "\n") { + line = strings.TrimSpace(line) + if strings.Contains(line, "Failed") || strings.Contains(line, "error") { + return nil, common.NewError("no certificate hash for ", server, ": ", line) + } + } return nil, common.NewError("no certificate hash found for ", server) } return leaves, nil