+
+ {t('pages.xray.outbound.egress')}:
+
+ {isEgressVisible ? (
+ setCardEgressVisible(rowKey, false)} onKeyDown={activateOnKey(() => setCardEgressVisible(rowKey, false))} />
+ ) : (
+ setCardEgressVisible(rowKey, true)} onKeyDown={activateOnKey(() => setCardEgressVisible(rowKey, true))} />
+ )}
+
+ {egress.country && (
+
+ )}
+
+ {addresses.map((addr) => (
+
+
+ {addr.label}:
+ {addr.value}
+
+
+ ))}
+
+ );
+ };
+
if (rows.length === 0) {
return (
@@ -101,6 +156,7 @@ export default function OutboundCardList({
))}
)}
+ {renderEgress(index, String(record.key))}
↑ {SizeFormatter.sizeFormat(trafficFor(outboundsTraffic, record).up)}
diff --git a/frontend/src/pages/xray/outbounds/OutboundsTab.css b/frontend/src/pages/xray/outbounds/OutboundsTab.css
index 85e9e2315..abbe1c996 100644
--- a/frontend/src/pages/xray/outbounds/OutboundsTab.css
+++ b/frontend/src/pages/xray/outbounds/OutboundsTab.css
@@ -75,6 +75,81 @@
background: var(--ant-color-fill-tertiary);
}
+.egress-header {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.egress-ip {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 12px;
+}
+
+.egress-stack {
+ display: inline-flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.egress-address {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ min-width: 0;
+}
+
+.egress-family {
+ color: var(--ant-color-text-secondary);
+ font-size: 10px;
+ text-transform: uppercase;
+}
+
+.country-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ border: 1px solid var(--ant-color-border);
+ border-radius: 4px;
+ padding: 1px 6px;
+ line-height: 20px;
+ font-size: 12px;
+ white-space: nowrap;
+}
+
+.country-pill.warp-on {
+ border-color: #f48120;
+}
+
+.warp-cloud-icon {
+ width: 1em;
+ height: 1em;
+ color: #f48120;
+ flex: 0 0 auto;
+}
+
+.warp-cloud-icon svg {
+ fill: #f48120;
+}
+
+.ip-toggle-icon {
+ cursor: pointer;
+ opacity: 0.65;
+}
+
+.ip-toggle-icon:hover {
+ opacity: 1;
+}
+
+.address-hidden {
+ filter: blur(5px);
+ transition: filter 0.2s ease;
+}
+
+.address-visible {
+ filter: none;
+}
+
.action-cell {
display: flex;
align-items: center;
@@ -110,6 +185,23 @@
flex-wrap: wrap;
}
+.card-egress {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 3px;
+ font-size: 12px;
+ opacity: 0.85;
+}
+
+.card-egress-row {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+ max-width: 100%;
+}
+
.card-test {
margin-left: auto;
display: inline-flex;
diff --git a/frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts b/frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts
index 311eacae8..f915e464d 100644
--- a/frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts
+++ b/frontend/src/pages/xray/outbounds/outbounds-tab-helpers.ts
@@ -61,6 +61,22 @@ export function trafficFor(outboundsTraffic: OutboundTrafficRow[], o: OutboundRo
return { up: tr?.up || 0, down: tr?.down || 0 };
}
+export function countryFlag(country?: string): string {
+ const code = (country || '').trim().toUpperCase();
+ if (!/^[A-Z]{2}$/.test(code)) return '';
+ return String.fromCodePoint(...[...code].map((ch) => 0x1f1e6 + ch.charCodeAt(0) - 65));
+}
+
+export function countryName(country?: string, locale?: string): string {
+ const code = (country || '').trim().toUpperCase();
+ if (!/^[A-Z]{2}$/.test(code)) return '';
+ try {
+ return new Intl.DisplayNames(locale ? [locale] : undefined, { type: 'region' }).of(code) || code;
+ } catch {
+ return code;
+ }
+}
+
export function isTesting
(states: Record, idx: K): boolean {
return !!states?.[idx]?.testing;
}
diff --git a/frontend/src/pages/xray/outbounds/useOutboundColumns.tsx b/frontend/src/pages/xray/outbounds/useOutboundColumns.tsx
index ad2111dc7..ab6f4e071 100644
--- a/frontend/src/pages/xray/outbounds/useOutboundColumns.tsx
+++ b/frontend/src/pages/xray/outbounds/useOutboundColumns.tsx
@@ -1,4 +1,4 @@
-import { useMemo } from 'react';
+import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Dropdown, Tag, Tooltip } from 'antd';
import {
@@ -11,17 +11,23 @@ import {
LoadingOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
+ EyeInvisibleOutlined,
+ EyeOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { SizeFormatter } from '@/utils';
+import { activateOnKey } from '@/utils/a11y';
import { OutboundProtocols as Protocols } from '@/schemas/primitives';
import type { OutboundTestMode, OutboundTestState, OutboundTrafficRow } from '@/hooks/useXraySetting';
import type { OutboundRow } from './outbounds-tab-types';
+import CountryPill from './CountryPill';
import TestResultPopover from './TestResultPopover';
import {
effectiveTestMode,
+ countryFlag,
+ countryName,
isTesting,
isUntestable,
outboundAddresses,
@@ -58,7 +64,8 @@ export function useOutboundColumns({
onResetTraffic,
onTest,
}: OutboundColumnsParams): ColumnsType {
- const { t } = useTranslation();
+ const { t, i18n } = useTranslation();
+ const [showEgressIp, setShowEgressIp] = useState(false);
return useMemo(
() => [
{
@@ -135,6 +142,72 @@ export function useOutboundColumns({
);
},
},
+ {
+ title: (
+
+ {t('pages.xray.outbound.egress')}
+
+ {showEgressIp ? (
+ setShowEgressIp(false)} onKeyDown={activateOnKey(() => setShowEgressIp(false))} />
+ ) : (
+ setShowEgressIp(true)} onKeyDown={activateOnKey(() => setShowEgressIp(true))} />
+ )}
+
+
+ ),
+ key: 'egress',
+ align: 'left',
+ width: 210,
+ render: (_v, _record, index) => {
+ const egress = testResult(outboundTestStates, index)?.egress;
+ const addresses = [
+ egress?.ipv4 ? { label: 'v4', value: egress.ipv4 } : null,
+ egress?.ipv6 ? { label: 'v6', value: egress.ipv6 } : null,
+ ].filter((item): item is { label: string; value: string } => Boolean(item));
+ if (addresses.length === 0) {
+ return (
+
+ —
+
+ );
+ }
+ return (
+
+ {addresses.map((addr) => (
+
+
+ {addr.label}
+ {addr.value}
+
+
+ ))}
+
+ );
+ },
+ },
+ {
+ title: t('pages.xray.outbound.country'),
+ key: 'egressCountry',
+ align: 'left',
+ width: 160,
+ render: (_v, _record, index) => {
+ const egress = testResult(outboundTestStates, index)?.egress;
+ if (!egress?.country) {
+ return (
+
+ —
+
+ );
+ }
+ const flag = countryFlag(egress.country);
+ const name = countryName(egress.country, i18n.language);
+ return (
+
+
+
+ );
+ },
+ },
{
title: t('pages.inbounds.traffic'),
key: 'traffic',
@@ -183,6 +256,6 @@ export function useOutboundColumns({
},
],
// eslint-disable-next-line react-hooks/exhaustive-deps
- [t, testMode, rows, outboundTestStates, outboundsTraffic],
+ [t, i18n.language, testMode, rows, outboundTestStates, outboundsTraffic, showEgressIp],
);
}
diff --git a/frontend/src/schemas/xray.ts b/frontend/src/schemas/xray.ts
index 8c1e36e57..2f9be4e71 100644
--- a/frontend/src/schemas/xray.ts
+++ b/frontend/src/schemas/xray.ts
@@ -78,6 +78,15 @@ export const OutboundTestResultSchema = z.object({
}).loose(),
)
.optional(),
+ egress: z
+ .object({
+ ipv4: z.string().optional(),
+ ipv6: z.string().optional(),
+ country: z.string().optional(),
+ warp: z.string().optional(),
+ })
+ .loose()
+ .optional(),
}).loose();
// Batch results from /xray/testOutbounds, aligned with the request order.
diff --git a/internal/web/service/outbound/egress_trace_test.go b/internal/web/service/outbound/egress_trace_test.go
new file mode 100644
index 000000000..3c85cd0a5
--- /dev/null
+++ b/internal/web/service/outbound/egress_trace_test.go
@@ -0,0 +1,17 @@
+package outbound
+
+import "testing"
+
+func TestParseCloudflareTrace(t *testing.T) {
+ values := parseCloudflareTrace("ip=104.28.1.2\nloc=NL\nwarp=on\n")
+
+ if values["ip"] != "104.28.1.2" {
+ t.Fatalf("ip = %q", values["ip"])
+ }
+ if values["loc"] != "NL" {
+ t.Fatalf("loc = %q", values["loc"])
+ }
+ if values["warp"] != "on" {
+ t.Fatalf("warp = %q", values["warp"])
+ }
+}
diff --git a/internal/web/service/outbound/outbound.go b/internal/web/service/outbound/outbound.go
index d37f8bf11..905093aae 100644
--- a/internal/web/service/outbound/outbound.go
+++ b/internal/web/service/outbound/outbound.go
@@ -142,6 +142,7 @@ type TestOutboundResult struct {
TTFBMs int64 `json:"ttfbMs,omitempty"`
Endpoints []TestEndpointResult `json:"endpoints,omitempty"`
+ Egress *TestEgressResult `json:"egress,omitempty"`
}
// TestEndpointResult is one entry in a TCP-mode probe — the per-endpoint
@@ -153,6 +154,15 @@ type TestEndpointResult struct {
Error string `json:"error,omitempty"`
}
+// TestEgressResult is populated by HTTP-mode probes from Cloudflare's trace
+// endpoint. It reports what an external service sees after the outbound chain.
+type TestEgressResult struct {
+ IPv4 string `json:"ipv4,omitempty"`
+ IPv6 string `json:"ipv6,omitempty"`
+ Country string `json:"country,omitempty"`
+ Warp string `json:"warp,omitempty"`
+}
+
func (s *OutboundService) testOutboundTCP(outboundJSON string) (*TestOutboundResult, error) {
var ob map[string]any
if err := json.Unmarshal([]byte(outboundJSON), &ob); err != nil {
diff --git a/internal/web/service/outbound/probe_http.go b/internal/web/service/outbound/probe_http.go
index aae6cd070..d47e568f3 100644
--- a/internal/web/service/outbound/probe_http.go
+++ b/internal/web/service/outbound/probe_http.go
@@ -14,6 +14,7 @@ import (
"net/url"
"os"
"strconv"
+ "strings"
"sync"
"time"
@@ -54,8 +55,13 @@ const (
// tcpBatchConcurrency caps parallel TCP-mode items in a batch (each item
// already dials its endpoints concurrently).
tcpBatchConcurrency = 8
+ // egressTraceTimeout keeps diagnostic trace metadata from extending a
+ // successful HTTP probe by the full reachability timeout.
+ egressTraceTimeout = 3 * time.Second
- defaultTestURL = "https://www.google.com/generate_204"
+ defaultTestURL = "https://www.google.com/generate_204"
+ egressTraceHost = "cloudflare.com"
+ egressTracePath = "/cdn-cgi/trace"
)
// httpTestSemaphore serialises HTTP-mode batches (each spawns a temp xray
@@ -76,6 +82,8 @@ var newBatchProcess = func(cfg *xray.Config, configPath string) batchProcess {
return xray.NewTestProcess(cfg, configPath)
}
+var egressTraceProbe = probeEgressTrace
+
// httpBatchItem is one outbound inside an HTTP-mode batch. result is the
// pre-allocated entry in the caller's result slice, filled in place.
type httpBatchItem struct {
@@ -542,6 +550,150 @@ func probeThroughSocks(port int, testURL string, timeout time.Duration, realDela
}
}
result.Delay = max(delay, 1)
+ if !realDelay {
+ result.Egress = egressTraceProbe(proxyURL)
+ }
+}
+
+// probeEgressTrace fetches Cloudflare's plain-text trace endpoint through the
+// same SOCKS route used by the HTTP probe. It asks one IPv4 and one IPv6
+// Cloudflare address directly, when available, while keeping the TLS SNI as
+// cloudflare.com. Failures are intentionally ignored by the caller: egress
+// metadata is diagnostic, not reachability.
+func probeEgressTrace(proxyURL *url.URL) *TestEgressResult {
+ ipv4, ipv6 := cloudflareTraceTargets()
+ if ipv4 == nil && ipv6 == nil {
+ return nil
+ }
+
+ tr := &http.Transport{
+ Proxy: http.ProxyURL(proxyURL),
+ TLSClientConfig: &tls.Config{
+ ServerName: egressTraceHost,
+ },
+ }
+ defer tr.CloseIdleConnections()
+
+ client := &http.Client{
+ Transport: tr,
+ Timeout: egressTraceTimeout,
+ }
+
+ egress := &TestEgressResult{}
+ targets := make([]net.IP, 0, 2)
+ if ipv4 != nil {
+ targets = append(targets, ipv4)
+ }
+ if ipv6 != nil {
+ targets = append(targets, ipv6)
+ }
+ results := make(chan map[string]string, len(targets))
+ for _, target := range targets {
+ go func(ip net.IP) {
+ results <- fetchCloudflareTrace(client, ip)
+ }(target)
+ }
+ for range targets {
+ applyEgressTrace(egress, <-results)
+ }
+ if egress.IPv4 == "" && egress.IPv6 == "" && egress.Country == "" && egress.Warp == "" {
+ return nil
+ }
+
+ return egress
+}
+
+func cloudflareTraceTargets() (net.IP, net.IP) {
+ ctx, cancel := context.WithTimeout(context.Background(), egressTraceTimeout)
+ defer cancel()
+
+ addrs, err := net.DefaultResolver.LookupIPAddr(ctx, egressTraceHost)
+ if err != nil {
+ return nil, nil
+ }
+ var ipv4, ipv6 net.IP
+ for _, addr := range addrs {
+ ip := addr.IP
+ if ipv4 == nil {
+ if v4 := ip.To4(); v4 != nil {
+ ipv4 = v4
+ continue
+ }
+ }
+ if ipv6 == nil && ip.To4() == nil && ip.To16() != nil {
+ ipv6 = ip
+ }
+ if ipv4 != nil && ipv6 != nil {
+ break
+ }
+ }
+ return ipv4, ipv6
+}
+
+func fetchCloudflareTrace(client *http.Client, ip net.IP) map[string]string {
+ if ip == nil {
+ return nil
+ }
+ traceURL := (&url.URL{
+ Scheme: "https",
+ Host: net.JoinHostPort(ip.String(), "443"),
+ Path: egressTracePath,
+ }).String()
+ req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, traceURL, nil)
+ if err != nil {
+ return nil
+ }
+ req.Host = egressTraceHost
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return nil
+ }
+ body, err := io.ReadAll(io.LimitReader(resp.Body, 16<<10))
+ if err != nil {
+ return nil
+ }
+ return parseCloudflareTrace(string(body))
+}
+
+func applyEgressTrace(egress *TestEgressResult, values map[string]string) {
+ if len(values) == 0 {
+ return
+ }
+ if ip := net.ParseIP(values["ip"]); ip != nil {
+ if ip.To4() != nil {
+ if egress.IPv4 == "" {
+ egress.IPv4 = values["ip"]
+ }
+ } else if egress.IPv6 == "" {
+ egress.IPv6 = values["ip"]
+ }
+ }
+ if egress.Country == "" {
+ egress.Country = values["loc"]
+ }
+ if values["warp"] == "on" || egress.Warp == "" {
+ egress.Warp = values["warp"]
+ }
+}
+
+func parseCloudflareTrace(body string) map[string]string {
+ values := make(map[string]string)
+ for line := range strings.SplitSeq(body, "\n") {
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ key, value, ok := strings.Cut(line, "=")
+ if !ok {
+ continue
+ }
+ values[strings.TrimSpace(key)] = strings.TrimSpace(value)
+ }
+ return values
}
// timedWarmGet re-issues the probe request over the transport's kept-alive
diff --git a/internal/web/service/outbound/probe_http_test.go b/internal/web/service/outbound/probe_http_test.go
index 816a1f4af..1c3ef4854 100644
--- a/internal/web/service/outbound/probe_http_test.go
+++ b/internal/web/service/outbound/probe_http_test.go
@@ -9,6 +9,7 @@ import (
"net"
"net/http"
"net/http/httptest"
+ "net/url"
"strconv"
"strings"
"sync"
@@ -135,6 +136,13 @@ func withStubProcess(t *testing.T, factory func(cfg *xray.Config, configPath str
t.Cleanup(func() { newBatchProcess = orig })
}
+func withEgressTraceProbe(t *testing.T, probe func(*url.URL) *TestEgressResult) {
+ t.Helper()
+ orig := egressTraceProbe
+ egressTraceProbe = probe
+ t.Cleanup(func() { egressTraceProbe = orig })
+}
+
func mustJSON(t *testing.T, v any) string {
t.Helper()
b, err := json.Marshal(v)
@@ -417,6 +425,9 @@ func TestTestOutboundsHTTPBatchThroughStubSocks(t *testing.T) {
proc = &stubProcess{cfg: cfg, serveSocks: true}
return proc
})
+ withEgressTraceProbe(t, func(*url.URL) *TestEgressResult {
+ return &TestEgressResult{IPv4: "198.51.100.1", Country: "ZZ", Warp: "off"}
+ })
batch := mustJSON(t, []any{
map[string]any{"tag": "a", "protocol": "vless"},
@@ -445,6 +456,9 @@ func TestTestOutboundsHTTPBatchThroughStubSocks(t *testing.T) {
if r.Mode != "http" {
t.Errorf("result %d mode = %q", i, r.Mode)
}
+ if r.Egress == nil || r.Egress.IPv4 != "198.51.100.1" {
+ t.Errorf("result %d egress = %+v", i, r.Egress)
+ }
}
if proc.IsRunning() {
t.Error("temp process not stopped after batch")
@@ -525,6 +539,9 @@ func TestTestOutboundsTCPModeForcesUDPToHTTPProbe(t *testing.T) {
withStubProcess(t, func(cfg *xray.Config, configPath string) batchProcess {
return &stubProcess{cfg: cfg, serveSocks: true}
})
+ withEgressTraceProbe(t, func(*url.URL) *TestEgressResult {
+ return &TestEgressResult{IPv4: "198.51.100.2", Country: "ZZ", Warp: "off"}
+ })
batch := mustJSON(t, []any{map[string]any{"tag": "wg", "protocol": "wireguard"}})
results, err := (&OutboundService{}).TestOutbounds(batch, srv.URL, "", "tcp")
@@ -535,6 +552,9 @@ func TestTestOutboundsTCPModeForcesUDPToHTTPProbe(t *testing.T) {
if !r.Success || r.Mode != "http" {
t.Errorf("UDP outbound in tcp mode = %+v, want success with mode %q", r, "http")
}
+ if r.Egress == nil || r.Egress.IPv4 != "198.51.100.2" {
+ t.Errorf("UDP outbound egress = %+v", r.Egress)
+ }
}
func TestProbeModeLabel(t *testing.T) {
diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json
index b7dd60ca2..45a08df0b 100644
--- a/internal/web/translation/ar-EG.json
+++ b/internal/web/translation/ar-EG.json
@@ -1622,6 +1622,8 @@
"tag": "الوسم",
"tagDesc": "تاج فريد",
"address": "العنوان",
+ "egress": "Egress",
+ "egressHint": "Run an HTTP test to show egress IP and country.",
"reverse": "عكسي",
"domain": "النطاق",
"type": "النوع",
diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json
index f4157901b..12bf23832 100644
--- a/internal/web/translation/en-US.json
+++ b/internal/web/translation/en-US.json
@@ -1739,6 +1739,8 @@
"tag": "Tag",
"tagDesc": "Unique Tag",
"address": "Address",
+ "egress": "Egress",
+ "egressHint": "Run an HTTP test to show egress IP and country.",
"reverse": "Reverse",
"domain": "Domain",
"type": "Type",
diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json
index 76f139059..62fe71f40 100644
--- a/internal/web/translation/es-ES.json
+++ b/internal/web/translation/es-ES.json
@@ -1622,6 +1622,8 @@
"tag": "Etiqueta",
"tagDesc": "etiqueta única",
"address": "Dirección",
+ "egress": "Egress",
+ "egressHint": "Run an HTTP test to show egress IP and country.",
"reverse": "Reverso",
"domain": "Dominio",
"type": "Tipo",
diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json
index 1c9c45bd5..422c1c159 100644
--- a/internal/web/translation/fa-IR.json
+++ b/internal/web/translation/fa-IR.json
@@ -1622,6 +1622,8 @@
"tag": "تگ",
"tagDesc": "برچسب یگانه",
"address": "آدرس",
+ "egress": "Egress",
+ "egressHint": "Run an HTTP test to show egress IP and country.",
"reverse": "معکوس",
"domain": "دامنه",
"type": "نوع",
diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json
index 295fe207e..d04530e55 100644
--- a/internal/web/translation/id-ID.json
+++ b/internal/web/translation/id-ID.json
@@ -1622,6 +1622,8 @@
"tag": "Tag",
"tagDesc": "Tag Unik",
"address": "Alamat",
+ "egress": "Egress",
+ "egressHint": "Run an HTTP test to show egress IP and country.",
"reverse": "Revers",
"domain": "Domain",
"type": "Tipe",
diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json
index 05c3de42a..d1a3117a7 100644
--- a/internal/web/translation/ja-JP.json
+++ b/internal/web/translation/ja-JP.json
@@ -1622,6 +1622,8 @@
"tag": "タグ",
"tagDesc": "一意のタグ",
"address": "アドレス",
+ "egress": "Egress",
+ "egressHint": "Run an HTTP test to show egress IP and country.",
"reverse": "リバース",
"domain": "ドメイン",
"type": "タイプ",
diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json
index 2b82826f2..4fba4cbdb 100644
--- a/internal/web/translation/pt-BR.json
+++ b/internal/web/translation/pt-BR.json
@@ -1622,6 +1622,8 @@
"tag": "Tag",
"tagDesc": "Tag Única",
"address": "Endereço",
+ "egress": "Egress",
+ "egressHint": "Run an HTTP test to show egress IP and country.",
"reverse": "Reverso",
"domain": "Domínio",
"type": "Tipo",
diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json
index c5c43ae43..339018bd6 100644
--- a/internal/web/translation/ru-RU.json
+++ b/internal/web/translation/ru-RU.json
@@ -1622,6 +1622,8 @@
"tag": "Тег",
"tagDesc": "Уникальный тег",
"address": "Адрес",
+ "egress": "Выход",
+ "egressHint": "Запустите HTTP-тест, чтобы показать выходной IP и страну.",
"reverse": "Реверс-прокси",
"domain": "Домен",
"type": "Тип",
diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json
index 4a8e523c8..eea477846 100644
--- a/internal/web/translation/tr-TR.json
+++ b/internal/web/translation/tr-TR.json
@@ -1622,6 +1622,8 @@
"tag": "Etiket",
"tagDesc": "Benzersiz Etiket",
"address": "Adres",
+ "egress": "Egress",
+ "egressHint": "Run an HTTP test to show egress IP and country.",
"reverse": "Ters",
"domain": "Alan Adı",
"type": "Tür",
diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json
index d9946d618..7cd8d65bd 100644
--- a/internal/web/translation/uk-UA.json
+++ b/internal/web/translation/uk-UA.json
@@ -1622,6 +1622,8 @@
"tag": "Тег",
"tagDesc": "Унікальний тег",
"address": "Адреса",
+ "egress": "Egress",
+ "egressHint": "Run an HTTP test to show egress IP and country.",
"reverse": "Зворотний",
"domain": "Домен",
"type": "Тип",
diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json
index 9ffe40e05..30588eda2 100644
--- a/internal/web/translation/vi-VN.json
+++ b/internal/web/translation/vi-VN.json
@@ -1622,6 +1622,8 @@
"tag": "Tag",
"tagDesc": "thẻ duy nhất",
"address": "Địa chỉ",
+ "egress": "Egress",
+ "egressHint": "Run an HTTP test to show egress IP and country.",
"reverse": "Đảo ngược",
"domain": "Tên miền",
"type": "Loại",
diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json
index 1303b270b..16e1b0d1e 100644
--- a/internal/web/translation/zh-CN.json
+++ b/internal/web/translation/zh-CN.json
@@ -1622,6 +1622,8 @@
"tag": "标签",
"tagDesc": "唯一标签",
"address": "地址",
+ "egress": "Egress",
+ "egressHint": "Run an HTTP test to show egress IP and country.",
"reverse": "反向",
"domain": "域名",
"type": "类型",
diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json
index 218ee21e2..46b3cddd7 100644
--- a/internal/web/translation/zh-TW.json
+++ b/internal/web/translation/zh-TW.json
@@ -1622,6 +1622,8 @@
"tag": "標籤",
"tagDesc": "唯一標籤",
"address": "地址",
+ "egress": "Egress",
+ "egressHint": "Run an HTTP test to show egress IP and country.",
"reverse": "反向",
"domain": "網域",
"type": "類型",