diff --git a/Changelog.md b/Changelog.md index 5d5b2b551..9aa3e0a67 100644 --- a/Changelog.md +++ b/Changelog.md @@ -46,7 +46,7 @@ ✨ 新增功能 - 新增连接页面按网络协议(TCP/UDP)筛选连接 -- 新增从连接详情快捷设置域名或进程规则,支持子域匹配与修改已有规则 +- 新增从连接详情快捷设置域名、进程或源/目标 IP 规则,支持子域匹配与修改已有规则 diff --git a/src/components/connection/connection-detail.tsx b/src/components/connection/connection-detail.tsx index 0bd9bea9a..80c3abc3c 100644 --- a/src/components/connection/connection-detail.tsx +++ b/src/components/connection/connection-detail.tsx @@ -8,14 +8,21 @@ import { closeConnection } from 'tauri-plugin-mihomo-api' import { useProfiles } from '@/hooks/use-profiles' import parseTraffic from '@/utils/parse-traffic' -import { getConnectionRuleTargets } from './connection-rule' +import { + getConnectionRuleTargets, + type ConnectionRuleKind, +} from './connection-rule' import { ConnectionRuleDialog, type ConnectionRuleTarget, } from './connection-rule-dialog' export interface ConnectionDetailRef { - open: (detail: IConnectionsItem, closed: boolean) => void + open: ( + detail: IConnectionsItem, + closed: boolean, + preferredKind?: ConnectionRuleKind, + ) => void close: () => void } @@ -23,6 +30,8 @@ export function ConnectionDetail({ ref }: { ref?: Ref }) { const [open, setOpen] = useState(false) const [detail, setDetail] = useState(null) const [closed, setClosed] = useState(false) + const [preferredKind, setPreferredKind] = + useState('domain') const theme = useTheme() const [ruleTarget, setRuleTarget] = useState( null, @@ -35,11 +44,16 @@ export function ConnectionDetail({ ref }: { ref?: Ref }) { }, []) useImperativeHandle(ref, () => ({ - open: (detail: IConnectionsItem, closed: boolean) => { + open: ( + detail: IConnectionsItem, + closed: boolean, + preferredKind = 'domain', + ) => { if (open) return setOpen(true) setDetail(detail) setClosed(closed) + setPreferredKind(preferredKind) }, close: onClose, })) @@ -67,6 +81,7 @@ export function ConnectionDetail({ ref }: { ref?: Ref }) { { setRuleTarget(target) @@ -89,6 +104,7 @@ export function ConnectionDetail({ ref }: { ref?: Ref }) { interface InnerProps { data: IConnectionsItem closed: boolean + preferredKind: ConnectionRuleKind onClose?: () => void onAddRule: (target: ConnectionRuleTarget) => void } @@ -96,6 +112,7 @@ interface InnerProps { const InnerConnectionDetail = ({ data, closed, + preferredKind, onClose, onAddRule, }: InnerProps) => { @@ -106,7 +123,10 @@ const InnerConnectionDetail = ({ const canAddRule = Boolean( current?.uid && current.option?.rules && - (targets.domain || targets.process), + (targets.domain || + targets.process || + targets.source || + targets.destination), ) const theme = useTheme() const chains = [...data.chains].reverse().join(' / ') @@ -211,6 +231,7 @@ const InnerConnectionDetail = ({ profileUid: current.uid, profileName: current.name || current.uid, rulesUid: current.option.rules, + preferredKind, }) }} > @@ -232,7 +253,10 @@ const InnerConnectionDetail = ({ {!canAddRule && ( {t( - targets.domain || targets.process + targets.domain || + targets.process || + targets.source || + targets.destination ? 'connections.ruleDialog.noProfile' : 'connections.ruleDialog.noMetadata', )} diff --git a/src/components/connection/connection-row-item.tsx b/src/components/connection/connection-row-item.tsx index 9eb5de4e0..2f9c6482f 100644 --- a/src/components/connection/connection-row-item.tsx +++ b/src/components/connection/connection-row-item.tsx @@ -7,11 +7,12 @@ import { closeConnection } from 'tauri-plugin-mihomo-api' import { RelativeTime } from './connection-relative-time' import type { ConnectionRowView } from './connection-row-view' +import type { ConnectionRuleKind } from './connection-rule' interface Props { row: ConnectionRowView closed: boolean - onShowDetail: (id: string) => void + onShowDetail: (id: string, preferredKind?: ConnectionRuleKind) => void } const tagStyle = { @@ -86,7 +87,17 @@ export const ConnectionRowItem = memo(
{row.network} {row.type} - {row.process && {row.process}} + {row.process && ( + { + event.stopPropagation() + onShowDetail(row.id, 'process') + }} + > + {row.process} + + )} {row.chains && {row.chains}} diff --git a/src/components/connection/connection-rule-dialog.tsx b/src/components/connection/connection-rule-dialog.tsx index 7e22f11d1..d31361ec8 100644 --- a/src/components/connection/connection-rule-dialog.tsx +++ b/src/components/connection/connection-rule-dialog.tsx @@ -30,6 +30,7 @@ import { getConnectionRuleTargets, findConnectionRule, saveConnectionRule, + type ConnectionRuleKind, } from './connection-rule' export interface ConnectionRuleTarget { @@ -37,9 +38,17 @@ export interface ConnectionRuleTarget { profileUid: string profileName: string rulesUid: string + preferredKind: ConnectionRuleKind } -type ConnectionRuleKind = 'domain' | 'process' +function getInitialKind(target: ConnectionRuleTarget) { + const targets = getConnectionRuleTargets(target.metadata) + return targets[target.preferredKind] + ? target.preferredKind + : ((['domain', 'process', 'source', 'destination'] as const).find( + (kind) => targets[kind], + ) ?? 'domain') +} export function ConnectionRuleDialog({ target, @@ -52,8 +61,8 @@ export function ConnectionRuleDialog({ const { proxyView } = useProxiesData() const { clashConfig } = useClashConfigData() const targets = getConnectionRuleTargets(target.metadata) - const [kind, setKind] = useState( - targets.domain ? 'domain' : 'process', + const [kind, setKind] = useState(() => + getInitialKind(target), ) const [policy, setPolicy] = useState(null) const [scope, setScope] = useState<'DOMAIN' | 'DOMAIN-SUFFIX'>('DOMAIN') @@ -83,6 +92,8 @@ export function ConnectionRuleDialog({ const kindLabels = { domain: t('connections.ruleDialog.domain'), process: t('connections.components.fields.process'), + source: t('connections.ruleDialog.sourceIp'), + destination: t('connections.ruleDialog.destinationIp'), } const unavailablePolicy = Boolean(policy && !policies.includes(policy)) @@ -106,9 +117,15 @@ export function ConnectionRuleDialog({ process: ruleTargets.process ? findConnectionRule(content, ruleTargets.process) : undefined, + source: ruleTargets.source + ? findConnectionRule(content, ruleTargets.source) + : undefined, + destination: ruleTargets.destination + ? findConnectionRule(content, ruleTargets.destination) + : undefined, } if (cancelled) return - const initialKind = ruleTargets.domain ? 'domain' : 'process' + const initialKind = getInitialKind(target) const initial = initialKind === 'domain' ? (rules.domain ?? rules.suffix) @@ -208,13 +225,19 @@ export function ConnectionRuleDialog({ ) }} > - {(['domain', 'process'] as const).map((option) => ( - - {kindLabels[option]} - {!targets[option] && - ` · ${t('connections.ruleDialog.unavailable')}`} - - ))} + {(['domain', 'process', 'source', 'destination'] as const).map( + (option) => ( + + {kindLabels[option]} + {!targets[option] && + ` · ${t('connections.ruleDialog.unavailable')}`} + + ), + )} {kind === 'process' ? ( <> @@ -284,6 +307,15 @@ export function ConnectionRuleDialog({ /> )} + {(kind === 'source' || kind === 'destination') && ( + + {t( + kind === 'source' + ? 'connections.ruleDialog.sourceHint' + : 'connections.ruleDialog.destinationHint', + )} + + )} {kind === 'process' && ( {selected?.type === 'PROCESS-PATH' diff --git a/src/components/connection/connection-rule.ts b/src/components/connection/connection-rule.ts index 1b42598dd..a75661cd6 100644 --- a/src/components/connection/connection-rule.ts +++ b/src/components/connection/connection-rule.ts @@ -2,10 +2,27 @@ import { dump, load } from 'js-yaml' import isFQDN from 'validator/es/lib/isFQDN' import isIP from 'validator/es/lib/isIP' +export type ConnectionRuleKind = 'domain' | 'process' | 'source' | 'destination' + +function getAddressRule(address: string | undefined, source: boolean) { + const ip = address?.trim() + if (!ip || !isIP(ip)) return undefined + const ipv4 = isIP(ip, 4) + return { + type: source ? 'SRC-IP-CIDR' : ipv4 ? 'IP-CIDR' : 'IP-CIDR6', + value: `${ip}/${ipv4 ? 32 : 128}`, + } +} + export function getConnectionRuleTargets( metadata: Pick< IConnectionsItem['metadata'], - 'host' | 'process' | 'processPath' + | 'host' + | 'process' + | 'processPath' + | 'sourceIP' + | 'destinationIP' + | 'remoteDestination' >, ) { const host = metadata.host?.trim() ?? '' @@ -20,7 +37,11 @@ export function getConnectionRuleTargets( : name?.trim() ? { type: 'PROCESS-NAME', value: name } : undefined - return { domain, process } + const source = getAddressRule(metadata.sourceIP, true) + const destination = + getAddressRule(metadata.destinationIP, false) ?? + getAddressRule(metadata.remoteDestination, false) + return { domain, process, source, destination } } function parseRuleExtension(content: string) { @@ -55,7 +76,11 @@ interface RuleCondition { function matchesConnectionRule(raw: string, target: RuleCondition) { const parts = raw.split(',').map((part) => part.trim()) - if (parts.length !== 3) return false + const hasNoResolve = + (target.type === 'IP-CIDR' || target.type === 'IP-CIDR6') && + parts.length === 4 && + parts[3] === 'no-resolve' + if (parts.length !== 3 && !hasNoResolve) return false if (target.type === 'DOMAIN' || target.type === 'DOMAIN-SUFFIX') { return ( parts[0] === target.type && @@ -92,7 +117,7 @@ export function saveConnectionRule( if (!matchesConnectionRule(raw, target)) return [raw] if (replaced) return [] replaced = true - return [rule] + return [raw.split(',').length === 4 ? `${rule},no-resolve` : rule] }) } if (!replaced) { diff --git a/src/components/connection/connection-table.tsx b/src/components/connection/connection-table.tsx index bb8d90dc6..18013ee08 100644 --- a/src/components/connection/connection-table.tsx +++ b/src/components/connection/connection-table.tsx @@ -29,6 +29,7 @@ import { getConnectionStartTime, getConnectionTypeLabel, } from './connection-row-view' +import type { ConnectionRuleKind } from './connection-rule' const ROW_HEIGHT = 40 const RESIZE_HANDLE_WIDTH = 6 @@ -247,7 +248,7 @@ const renderCell = ( interface RowComponentProps { row: IConnectionsItem columns: DisplayColumn[] - onShowDetail: (id: string) => void + onShowDetail: (id: string, preferredKind?: ConnectionRuleKind) => void getSnapshot: (row: IConnectionsItem) => TableRowSnapshot borderColor: string virtualTop: number @@ -263,7 +264,22 @@ const RowComponent = memo( virtualTop, }: RowComponentProps) { const handleClick = useCallback( - () => onShowDetail(row.id), + (event: ReactMouseEvent) => { + const field = + event.target instanceof Element + ? event.target.closest('[data-connection-field]') + ?.dataset.connectionField + : undefined + const preferredKind = + field === 'process' + ? 'process' + : field === 'source' + ? 'source' + : field === 'remoteDestination' + ? 'destination' + : 'domain' + onShowDetail(row.id, preferredKind) + }, [onShowDetail, row.id], ) const snapshot = getSnapshot(row) @@ -285,6 +301,7 @@ const RowComponent = memo( {columns.map((column) => (
void + onShowDetail: (id: string, preferredKind?: ConnectionRuleKind) => void columnManagerOpen: boolean onCloseColumnManager: () => void } @@ -333,7 +350,8 @@ export const ConnectionTable = (props: Props) => { const onShowDetailRef = useRef(rawOnShowDetail) onShowDetailRef.current = rawOnShowDetail const onShowDetail = useCallback( - (id: string) => onShowDetailRef.current(id), + (id: string, preferredKind?: ConnectionRuleKind) => + onShowDetailRef.current(id, preferredKind), [], ) const { t } = useTranslation() diff --git a/src/locales/ar/connections.json b/src/locales/ar/connections.json index 370c63e79..24696ce9f 100644 --- a/src/locales/ar/connections.json +++ b/src/locales/ar/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/locales/de/connections.json b/src/locales/de/connections.json index d62a076cc..8c4806587 100644 --- a/src/locales/de/connections.json +++ b/src/locales/de/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/locales/en/connections.json b/src/locales/en/connections.json index 09b9854b5..ea8f9686a 100644 --- a/src/locales/en/connections.json +++ b/src/locales/en/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/locales/es/connections.json b/src/locales/es/connections.json index ee8eef59c..8c27586a6 100644 --- a/src/locales/es/connections.json +++ b/src/locales/es/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/locales/fa/connections.json b/src/locales/fa/connections.json index 7bd73b7c5..2b1ba8364 100644 --- a/src/locales/fa/connections.json +++ b/src/locales/fa/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/locales/id/connections.json b/src/locales/id/connections.json index 3757fc227..e8f0a299f 100644 --- a/src/locales/id/connections.json +++ b/src/locales/id/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/locales/jp/connections.json b/src/locales/jp/connections.json index c633e02d7..018bb9abe 100644 --- a/src/locales/jp/connections.json +++ b/src/locales/jp/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/locales/ko/connections.json b/src/locales/ko/connections.json index 235fb8916..3b834e12d 100644 --- a/src/locales/ko/connections.json +++ b/src/locales/ko/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/locales/pt/connections.json b/src/locales/pt/connections.json index 25ec983c7..6678de1ee 100644 --- a/src/locales/pt/connections.json +++ b/src/locales/pt/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/locales/ru/connections.json b/src/locales/ru/connections.json index da41ee087..d59d06983 100644 --- a/src/locales/ru/connections.json +++ b/src/locales/ru/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/locales/tr/connections.json b/src/locales/tr/connections.json index 012e435ee..ac5a24cf7 100644 --- a/src/locales/tr/connections.json +++ b/src/locales/tr/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/locales/tt/connections.json b/src/locales/tt/connections.json index cea7dada1..8b43373f3 100644 --- a/src/locales/tt/connections.json +++ b/src/locales/tt/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/locales/zh/connections.json b/src/locales/zh/connections.json index 94954042b..0dc379ca1 100644 --- a/src/locales/zh/connections.json +++ b/src/locales/zh/connections.json @@ -46,7 +46,7 @@ "applyFailed": "规则未能应用,已保留当前选择。", "unsupportedValue": "此内容包含快捷规则不支持的字符。", "saved": "规则已保存,配置已应用。", - "noMetadata": "此连接没有可用于添加规则的域名或进程信息。", + "noMetadata": "此连接没有可用于设置规则的域名、进程或 IP 信息。", "noProfile": "当前没有可编辑规则扩展的活动订阅。", "entry": "设置规则…", "modifyTitle": "修改规则", @@ -56,6 +56,10 @@ "policyUnavailable": "此策略已不可用,请选择其他策略。", "editDestination": "修改「{{name}}」中的现有规则,保留排序。", "saveAndApply": "保存并应用", + "sourceIp": "源 IP", + "destinationIp": "目标 IP", + "sourceHint": "匹配来自此 IP 的所有请求,可能包含其他应用,不限定端口。", + "destinationHint": "匹配访问此目标 IP 的请求,覆盖所有域名,不限定端口。", "fullPath": "完整路径" } } diff --git a/src/locales/zhtw/connections.json b/src/locales/zhtw/connections.json index 4b6c094ed..574d109ed 100644 --- a/src/locales/zhtw/connections.json +++ b/src/locales/zhtw/connections.json @@ -46,7 +46,7 @@ "applyFailed": "The rule could not be applied. Your selection has been kept.", "unsupportedValue": "This value contains characters that cannot be used in a quick rule.", "saved": "Rule saved and configuration applied.", - "noMetadata": "This connection has no domain or process information for adding a rule.", + "noMetadata": "This connection has no domain, process or IP information for configuring a rule.", "noProfile": "No active profile with an editable rule extension is available.", "entry": "Configure rule…", "modifyTitle": "Modify rule", @@ -56,6 +56,10 @@ "policyUnavailable": "This policy is no longer available. Choose another policy.", "editDestination": "Update the existing rule in “{{name}}”, keeping its position.", "saveAndApply": "Save and apply", + "sourceIp": "Source IP", + "destinationIp": "Destination IP", + "sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.", + "destinationHint": "Matches this destination IP across all domains. No port filtering.", "fullPath": "Full path" } } diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 7a11a1497..80df49cb8 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -35,6 +35,7 @@ import { getConnectionStartTime, useConnectionRowViews, } from '@/components/connection/connection-row-view' +import type { ConnectionRuleKind } from '@/components/connection/connection-rule' import { ConnectionTable } from '@/components/connection/connection-table' import { useConnectionData } from '@/hooks/use-connection-data' import { useConnectionSetting } from '@/hooks/use-connection-setting' @@ -161,10 +162,14 @@ const ConnectionsPage = () => { ) const showDetailById = useCallback( - (id: string) => { + (id: string, preferredKind: ConnectionRuleKind = 'domain') => { const connection = filterConn.find((item) => item.id === id) if (connection) { - detailRef.current?.open(connection, connectionsType === 'closed') + detailRef.current?.open( + connection, + connectionsType === 'closed', + preferredKind, + ) } }, [connectionsType, filterConn], diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index 22a4ad9b2..2440917a8 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -46,6 +46,10 @@ export const translationKeys = [ 'connections.ruleDialog.policyUnavailable', 'connections.ruleDialog.editDestination', 'connections.ruleDialog.saveAndApply', + 'connections.ruleDialog.sourceIp', + 'connections.ruleDialog.destinationIp', + 'connections.ruleDialog.sourceHint', + 'connections.ruleDialog.destinationHint', 'connections.ruleDialog.fullPath', 'home.page.tooltips.lightweightMode', 'home.page.tooltips.manual', diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index a3c90b136..3a97e0a2e 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -41,6 +41,8 @@ export interface TranslationResources { allSubdomains: string applyFailed: string destination: string + destinationHint: string + destinationIp: string domain: string domainScope: string editDestination: string @@ -59,6 +61,8 @@ export interface TranslationResources { ruleModeHint: string saveAndApply: string saved: string + sourceHint: string + sourceIp: string title: string unavailable: string unsupportedValue: string