mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 10:14:15 +00:00
fix(xray): test UDP outbounds via xray probe (#4657) + Vision testseed & Flow form fixes
Outbound connection tester (#4657): UDP-based outbounds (wireguard, hysteria, kcp/quic transports) were probed with a raw UDP dial that treated the inevitable read timeout as success, so every one reported a fake ~5s 'alive'. Route them through the authoritative xray burstObservatory probe and drop the broken raw-UDP path. Test All now runs a parallel TCP lane and a serial HTTP lane so xray-probe outbounds don't collide on the test semaphore. Vision testseed: the [900, 500, 900, 256] default repeats 900, and a tags Select keys each tag by value -> 'two children with the same key, 900'. Render it as four InputNumbers (inbound + outbound forms); the field is a fixed 4-tuple where repeats are valid. Inbound form: drop the null-valued 'Local Panel' Select option (AntD rejects null option values; placeholder + allowClear already cover it). Outbound form: add an explicit 'None' option to the Flow selector.
This commit is contained in:
parent
8c30ddbfd9
commit
cb7af04cd3
5 changed files with 88 additions and 133 deletions
|
|
@ -17,6 +17,13 @@ import {
|
||||||
const DIRTY_POLL_MS = 1000;
|
const DIRTY_POLL_MS = 1000;
|
||||||
const DEFAULT_TEST_URL = 'https://www.google.com/generate_204';
|
const DEFAULT_TEST_URL = 'https://www.google.com/generate_204';
|
||||||
|
|
||||||
|
export function isUdpOutbound(outbound: unknown): boolean {
|
||||||
|
const o = outbound as { protocol?: string; streamSettings?: { network?: string } } | null | undefined;
|
||||||
|
const p = o?.protocol;
|
||||||
|
const n = o?.streamSettings?.network;
|
||||||
|
return p === 'wireguard' || p === 'hysteria' || n === 'hysteria' || n === 'kcp' || n === 'quic';
|
||||||
|
}
|
||||||
|
|
||||||
export type { OutboundTrafficRow, OutboundTestResult };
|
export type { OutboundTrafficRow, OutboundTestResult };
|
||||||
|
|
||||||
export type XraySettingsValue = z.infer<typeof XraySettingsValueSchema>;
|
export type XraySettingsValue = z.infer<typeof XraySettingsValueSchema>;
|
||||||
|
|
@ -243,15 +250,16 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
const testOutbound = useCallback(
|
const testOutbound = useCallback(
|
||||||
async (index: number, outbound: unknown, mode = 'tcp'): Promise<OutboundTestResult | null> => {
|
async (index: number, outbound: unknown, mode = 'tcp'): Promise<OutboundTestResult | null> => {
|
||||||
if (!outbound) return null;
|
if (!outbound) return null;
|
||||||
|
const effMode = isUdpOutbound(outbound) ? 'http' : mode;
|
||||||
setOutboundTestStates((prev) => ({
|
setOutboundTestStates((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[index]: { testing: true, result: null, mode },
|
[index]: { testing: true, result: null, mode: effMode },
|
||||||
}));
|
}));
|
||||||
try {
|
try {
|
||||||
const raw = await HttpUtil.post('/panel/xray/testOutbound', {
|
const raw = await HttpUtil.post('/panel/xray/testOutbound', {
|
||||||
outbound: JSON.stringify(outbound),
|
outbound: JSON.stringify(outbound),
|
||||||
allOutbounds: JSON.stringify(templateSettingsRef.current?.outbounds || []),
|
allOutbounds: JSON.stringify(templateSettingsRef.current?.outbounds || []),
|
||||||
mode,
|
mode: effMode,
|
||||||
});
|
});
|
||||||
const msg = parseMsg(raw, OutboundTestResultSchema, 'xray/testOutbound');
|
const msg = parseMsg(raw, OutboundTestResultSchema, 'xray/testOutbound');
|
||||||
if (msg?.success && msg.obj) {
|
if (msg?.success && msg.obj) {
|
||||||
|
|
@ -265,7 +273,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
...prev,
|
...prev,
|
||||||
[index]: {
|
[index]: {
|
||||||
testing: false,
|
testing: false,
|
||||||
result: { success: false, error: msg?.msg || 'Unknown error', mode },
|
result: { success: false, error: msg?.msg || 'Unknown error', mode: effMode },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -273,7 +281,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
...prev,
|
...prev,
|
||||||
[index]: {
|
[index]: {
|
||||||
testing: false,
|
testing: false,
|
||||||
result: { success: false, error: String(e), mode },
|
result: { success: false, error: String(e), mode: effMode },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -287,28 +295,31 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
if (list.length === 0 || testingAll) return;
|
if (list.length === 0 || testingAll) return;
|
||||||
setTestingAll(true);
|
setTestingAll(true);
|
||||||
try {
|
try {
|
||||||
const concurrency = mode === 'tcp' ? 8 : 1;
|
const tcpQueue: { index: number; outbound: unknown }[] = [];
|
||||||
const queue = list
|
const httpQueue: { index: number; outbound: unknown }[] = [];
|
||||||
.map((ob, i) => ({ index: i, outbound: ob }))
|
list.forEach((ob, i) => {
|
||||||
.filter(({ outbound }) => {
|
const tag = ob?.tag;
|
||||||
const tag = outbound?.tag;
|
const proto = ob?.protocol;
|
||||||
const proto = outbound?.protocol;
|
if (proto === 'blackhole' || proto === 'loopback' || tag === 'blocked') return;
|
||||||
if (proto === 'blackhole' || proto === 'loopback' || tag === 'blocked') return false;
|
if (mode === 'tcp' && (proto === 'freedom' || proto === 'dns')) return;
|
||||||
if (mode === 'tcp' && (proto === 'freedom' || proto === 'dns')) return false;
|
if (mode === 'http' || isUdpOutbound(ob)) {
|
||||||
return true;
|
httpQueue.push({ index: i, outbound: ob });
|
||||||
|
} else {
|
||||||
|
tcpQueue.push({ index: i, outbound: ob });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
async function worker() {
|
const runLane = async (queue: { index: number; outbound: unknown }[], concurrency: number) => {
|
||||||
|
const worker = async () => {
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const item = queue.shift();
|
const item = queue.shift();
|
||||||
if (!item) break;
|
if (!item) break;
|
||||||
await testOutbound(item.index, item.outbound, mode);
|
await testOutbound(item.index, item.outbound, mode);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
const workers = Array.from(
|
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, () => worker());
|
||||||
{ length: Math.min(concurrency, queue.length) },
|
|
||||||
() => worker(),
|
|
||||||
);
|
|
||||||
await Promise.all(workers);
|
await Promise.all(workers);
|
||||||
|
};
|
||||||
|
await Promise.all([runLane(tcpQueue, 8), runLane(httpQueue, 1)]);
|
||||||
} finally {
|
} finally {
|
||||||
setTestingAll(false);
|
setTestingAll(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -931,14 +931,11 @@ export default function InboundFormModal({
|
||||||
disabled={mode === 'edit'}
|
disabled={mode === 'edit'}
|
||||||
placeholder={t('pages.inbounds.localPanel')}
|
placeholder={t('pages.inbounds.localPanel')}
|
||||||
allowClear
|
allowClear
|
||||||
options={[
|
options={selectableNodes.map((n) => ({
|
||||||
{ value: null, label: t('pages.inbounds.localPanel') },
|
|
||||||
...selectableNodes.map((n) => ({
|
|
||||||
value: n.id,
|
value: n.id,
|
||||||
label: `${n.name}${n.status === 'offline' ? ' (offline)' : ''}`,
|
label: `${n.name}${n.status === 'offline' ? ' (offline)' : ''}`,
|
||||||
disabled: n.status === 'offline',
|
disabled: n.status === 'offline',
|
||||||
})),
|
}))}
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1498,16 +1495,15 @@ export default function InboundFormModal({
|
||||||
{network === 'tcp' && (security === 'tls' || security === 'reality') && (
|
{network === 'tcp' && (security === 'tls' || security === 'reality') && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('pages.inbounds.form.visionTestseed')}
|
label={t('pages.inbounds.form.visionTestseed')}
|
||||||
name={['settings', 'testseed']}
|
|
||||||
initialValue={[900, 500, 900, 256]}
|
|
||||||
normalize={(v: unknown) =>
|
|
||||||
Array.isArray(v)
|
|
||||||
? v.map((x) => Number(x)).filter((n) => Number.isInteger(n) && n > 0)
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
extra="Applies only to clients using the xtls-rprx-vision flow; ignored otherwise."
|
extra="Applies only to clients using the xtls-rprx-vision flow; ignored otherwise."
|
||||||
>
|
>
|
||||||
<Select mode="tags" tokenSeparators={[',', ' ']} placeholder="four positive integers" />
|
<Space.Compact block>
|
||||||
|
{[900, 500, 900, 256].map((def, i) => (
|
||||||
|
<Form.Item key={i} name={['settings', 'testseed', i]} noStyle initialValue={def}>
|
||||||
|
<InputNumber min={1} style={{ width: '25%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -191,8 +191,8 @@ export default function OutboundFormModal({
|
||||||
const [linkInput, setLinkInput] = useState('');
|
const [linkInput, setLinkInput] = useState('');
|
||||||
|
|
||||||
// Parse a share link (vmess:// / vless:// / trojan:// / ss:// /
|
// Parse a share link (vmess:// / vless:// / trojan:// / ss:// /
|
||||||
// hysteria2://) and replace form state with the result. The current
|
// hysteria2:// / wireguard://) and replace form state with the result.
|
||||||
// tag is preserved when the parsed link doesn't carry one.
|
// The current tag is preserved when the parsed link doesn't carry one.
|
||||||
function importLink() {
|
function importLink() {
|
||||||
const link = linkInput.trim();
|
const link = linkInput.trim();
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|
@ -1743,7 +1743,7 @@ export default function OutboundFormModal({
|
||||||
<Select
|
<Select
|
||||||
allowClear
|
allowClear
|
||||||
placeholder={t('none')}
|
placeholder={t('none')}
|
||||||
options={FLOW_OPTIONS}
|
options={[{ value: '', label: t('none') }, ...FLOW_OPTIONS]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1762,22 +1762,14 @@ export default function OutboundFormModal({
|
||||||
<Form.Item label={t('pages.xray.outboundForm.visionTestpre')} name={['settings', 'testpre']}>
|
<Form.Item label={t('pages.xray.outboundForm.visionTestpre')} name={['settings', 'testpre']}>
|
||||||
<InputNumber min={0} style={{ width: '100%' }} />
|
<InputNumber min={0} style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t('pages.inbounds.form.visionTestseed')}>
|
||||||
label={t('pages.inbounds.form.visionTestseed')}
|
<Space.Compact block>
|
||||||
name={['settings', 'testseed']}
|
{[900, 500, 900, 256].map((def, i) => (
|
||||||
normalize={(v: unknown) =>
|
<Form.Item key={i} name={['settings', 'testseed', i]} noStyle initialValue={def}>
|
||||||
Array.isArray(v)
|
<InputNumber min={1} style={{ width: '25%' }} />
|
||||||
? v
|
</Form.Item>
|
||||||
.map((x) => Number(x))
|
))}
|
||||||
.filter((n) => Number.isInteger(n) && n > 0)
|
</Space.Compact>
|
||||||
: []
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
mode="tags"
|
|
||||||
tokenSeparators={[',', ' ']}
|
|
||||||
placeholder="four positive integers"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -2215,7 +2207,7 @@ export default function OutboundFormModal({
|
||||||
<Space orientation="vertical" size={10} style={{ width: '100%', marginTop: 10 }}>
|
<Space orientation="vertical" size={10} style={{ width: '100%', marginTop: 10 }}>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
value={linkInput}
|
value={linkInput}
|
||||||
placeholder="vmess:// vless:// trojan:// ss:// hysteria2://"
|
placeholder="vmess:// vless:// trojan:// ss:// hysteria2:// wireguard://"
|
||||||
enterButton="Import"
|
enterButton="Import"
|
||||||
onChange={(e) => setLinkInput(e.target.value)}
|
onChange={(e) => setLinkInput(e.target.value)}
|
||||||
onSearch={importLink}
|
onSearch={importLink}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import type { ColumnsType } from 'antd/es/table';
|
||||||
import { SizeFormatter } from '@/utils';
|
import { SizeFormatter } from '@/utils';
|
||||||
import { OutboundProtocols as Protocols } from '@/schemas/primitives';
|
import { OutboundProtocols as Protocols } from '@/schemas/primitives';
|
||||||
import OutboundFormModal from './OutboundFormModal';
|
import OutboundFormModal from './OutboundFormModal';
|
||||||
|
import { isUdpOutbound } from '@/hooks/useXraySetting';
|
||||||
import type { XraySettingsValue, SetTemplate, OutboundTestState, OutboundTrafficRow } from '@/hooks/useXraySetting';
|
import type { XraySettingsValue, SetTemplate, OutboundTestState, OutboundTrafficRow } from '@/hooks/useXraySetting';
|
||||||
import './OutboundsTab.css';
|
import './OutboundsTab.css';
|
||||||
|
|
||||||
|
|
@ -361,7 +362,7 @@ export default function OutboundsTab({
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (_v, record, index) => (
|
render: (_v, record, index) => (
|
||||||
<Tooltip title={`${t('check')} (${testMode.toUpperCase()})`}>
|
<Tooltip title={`${t('check')} (${(isUdpOutbound(record) ? 'http' : testMode).toUpperCase()})`}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,14 @@ type TestEndpointResult struct {
|
||||||
// sockopt.dialerProxy chains during test).
|
// sockopt.dialerProxy chains during test).
|
||||||
func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allOutboundsJSON string, mode string) (*TestOutboundResult, error) {
|
func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allOutboundsJSON string, mode string) (*TestOutboundResult, error) {
|
||||||
if mode == "tcp" {
|
if mode == "tcp" {
|
||||||
|
// A bare TCP dial only proves reachability for TCP-based proxies.
|
||||||
|
// UDP protocols (wireguard, hysteria, kcp/quic transports) ignore
|
||||||
|
// unauthenticated packets, so a raw dial can't tell "reachable" from
|
||||||
|
// "dead" — route them through the authoritative xray handshake probe.
|
||||||
|
var ob map[string]any
|
||||||
|
if json.Unmarshal([]byte(outboundJSON), &ob) == nil && outboundTransportIsUDP(ob) {
|
||||||
|
return s.testOutboundHTTP(outboundJSON, testURL, allOutboundsJSON)
|
||||||
|
}
|
||||||
return s.testOutboundTCP(outboundJSON)
|
return s.testOutboundTCP(outboundJSON)
|
||||||
}
|
}
|
||||||
return s.testOutboundHTTP(outboundJSON, testURL, allOutboundsJSON)
|
return s.testOutboundHTTP(outboundJSON, testURL, allOutboundsJSON)
|
||||||
|
|
@ -178,7 +186,7 @@ func (s *OutboundService) testOutboundTCP(outboundJSON string) (*TestOutboundRes
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(i int) {
|
go func(i int) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
results[i] = probeEndpoint(endpoints[i], 5*time.Second)
|
results[i] = probeTCPEndpoint(endpoints[i], 5*time.Second)
|
||||||
}(i)
|
}(i)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
@ -195,11 +203,7 @@ func (s *OutboundService) testOutboundTCP(outboundJSON string) (*TestOutboundRes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mode := "tcp"
|
out := &TestOutboundResult{Mode: "tcp", Endpoints: results}
|
||||||
if endpoints[0].Network == "udp" {
|
|
||||||
mode = "udp"
|
|
||||||
}
|
|
||||||
out := &TestOutboundResult{Mode: mode, Endpoints: results}
|
|
||||||
if bestDelay >= 0 {
|
if bestDelay >= 0 {
|
||||||
out.Success = true
|
out.Success = true
|
||||||
out.Delay = bestDelay
|
out.Delay = bestDelay
|
||||||
|
|
@ -212,22 +216,6 @@ func (s *OutboundService) testOutboundTCP(outboundJSON string) (*TestOutboundRes
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// outboundEndpoint is a host:port plus the transport its proxy actually
|
|
||||||
// listens on. WireGuard (and WARP, which is WireGuard) is UDP-only, so a
|
|
||||||
// TCP dial to its peer endpoint always times out — the probe must match
|
|
||||||
// the transport of the outbound being tested.
|
|
||||||
type outboundEndpoint struct {
|
|
||||||
Address string
|
|
||||||
Network string
|
|
||||||
}
|
|
||||||
|
|
||||||
func probeEndpoint(ep outboundEndpoint, timeout time.Duration) TestEndpointResult {
|
|
||||||
if ep.Network == "udp" {
|
|
||||||
return probeUDPEndpoint(ep.Address, timeout)
|
|
||||||
}
|
|
||||||
return probeTCPEndpoint(ep.Address, timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func probeTCPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult {
|
func probeTCPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult {
|
||||||
r := TestEndpointResult{Address: endpoint}
|
r := TestEndpointResult{Address: endpoint}
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
@ -242,69 +230,36 @@ func probeTCPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// probeUDPEndpoint sends a single byte and waits briefly for a reply or
|
// outboundTransportIsUDP reports whether the outbound's proxy speaks UDP
|
||||||
// an ICMP-driven error. WireGuard won't answer an unauthenticated byte,
|
// (wireguard, hysteria, or a kcp/quic/hysteria stream transport). A bare
|
||||||
// so a read timeout is the normal "endpoint reachable" outcome; a
|
// UDP dial can't probe these — they ignore unauthenticated packets, so a
|
||||||
// concrete error (e.g. ECONNREFUSED, "host unreachable") fails the probe.
|
// dial neither proves reachability nor measures latency. Such outbounds
|
||||||
func probeUDPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult {
|
// must go through the real xray handshake probe instead.
|
||||||
r := TestEndpointResult{Address: endpoint}
|
func outboundTransportIsUDP(ob map[string]any) bool {
|
||||||
start := time.Now()
|
if protocol, _ := ob["protocol"].(string); protocol == "hysteria" || protocol == "wireguard" {
|
||||||
conn, err := net.DialTimeout("udp", endpoint, timeout)
|
return true
|
||||||
if err != nil {
|
|
||||||
r.Delay = time.Since(start).Milliseconds()
|
|
||||||
r.Error = err.Error()
|
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
if stream, ok := ob["streamSettings"].(map[string]any); ok {
|
||||||
|
if n, _ := stream["network"].(string); n == "hysteria" || n == "kcp" || n == "quic" {
|
||||||
if _, werr := conn.Write([]byte{0}); werr != nil {
|
return true
|
||||||
r.Delay = time.Since(start).Milliseconds()
|
|
||||||
r.Error = werr.Error()
|
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
|
||||||
buf := make([]byte, 64)
|
|
||||||
_, rerr := conn.Read(buf)
|
|
||||||
r.Delay = time.Since(start).Milliseconds()
|
|
||||||
if rerr != nil {
|
|
||||||
if nerr, ok := rerr.(net.Error); ok && nerr.Timeout() {
|
|
||||||
r.Success = true
|
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
r.Error = rerr.Error()
|
return false
|
||||||
return r
|
|
||||||
}
|
|
||||||
r.Success = true
|
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractOutboundEndpoints(ob map[string]any) []outboundEndpoint {
|
func extractOutboundEndpoints(ob map[string]any) []string {
|
||||||
protocol, _ := ob["protocol"].(string)
|
protocol, _ := ob["protocol"].(string)
|
||||||
settings, _ := ob["settings"].(map[string]any)
|
settings, _ := ob["settings"].(map[string]any)
|
||||||
if settings == nil {
|
if settings == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hysteria is QUIC/UDP — detect via the outer protocol or via
|
var out []string
|
||||||
// streamSettings.network so a trojan-with-hysteria transport gets
|
|
||||||
// probed over UDP too. kcp and quic are also UDP-based.
|
|
||||||
network := "tcp"
|
|
||||||
if protocol == "hysteria" || protocol == "wireguard" {
|
|
||||||
network = "udp"
|
|
||||||
}
|
|
||||||
if stream, ok := ob["streamSettings"].(map[string]any); ok {
|
|
||||||
if n, _ := stream["network"].(string); n == "hysteria" || n == "kcp" || n == "quic" {
|
|
||||||
network = "udp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var out []outboundEndpoint
|
|
||||||
addServer := func(addr any, port any) {
|
addServer := func(addr any, port any) {
|
||||||
host, _ := addr.(string)
|
host, _ := addr.(string)
|
||||||
p := numAsInt(port)
|
p := numAsInt(port)
|
||||||
if host != "" && p > 0 {
|
if host != "" && p > 0 {
|
||||||
out = append(out, outboundEndpoint{Address: fmt.Sprintf("%s:%d", host, p), Network: network})
|
out = append(out, fmt.Sprintf("%s:%d", host, p))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch protocol {
|
switch protocol {
|
||||||
|
|
@ -333,7 +288,7 @@ func extractOutboundEndpoints(ob map[string]any) []outboundEndpoint {
|
||||||
for _, p := range peers {
|
for _, p := range peers {
|
||||||
if pm, ok := p.(map[string]any); ok {
|
if pm, ok := p.(map[string]any); ok {
|
||||||
if ep, _ := pm["endpoint"].(string); ep != "" {
|
if ep, _ := pm["endpoint"].(string); ep != "" {
|
||||||
out = append(out, outboundEndpoint{Address: ep, Network: network})
|
out = append(out, ep)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue