From d0bb9165bf1f42634a088982281373a88287a389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=8C=E5=90=91=E6=9E=81=E7=AB=AF?= Date: Thu, 2 Oct 2025 20:33:20 +0800 Subject: [PATCH] feat: Add inbound interface restriction option for ufw forward rules (#10345) (#10549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复https防窜站关闭时修改默认站点报错的问题 * feat: Add inbound interface restriction option for ufw forward rules (#10345) --------- Co-authored-by: live --- agent/app/dto/firewall.go | 1 + agent/app/model/firewall.go | 1 + agent/app/service/firewall.go | 7 ++- agent/init/migration/migrations/init.go | 2 +- agent/utils/firewall/client/info.go | 4 ++ agent/utils/firewall/client/iptables.go | 62 ++++++++++--------- agent/utils/firewall/client/ufw.go | 5 +- frontend/src/api/interface/dashboard.ts | 2 +- frontend/src/api/interface/host.ts | 1 + frontend/src/api/interface/setting.ts | 2 +- frontend/src/api/modules/host.ts | 2 +- frontend/src/lang/modules/en.ts | 1 + frontend/src/lang/modules/es-es.ts | 1 + frontend/src/lang/modules/ja.ts | 1 + frontend/src/lang/modules/ko.ts | 1 + frontend/src/lang/modules/ms.ts | 1 + frontend/src/lang/modules/pt-br.ts | 1 + frontend/src/lang/modules/ru.ts | 1 + frontend/src/lang/modules/tr.ts | 1 + frontend/src/lang/modules/zh-Hant.ts | 1 + frontend/src/lang/modules/zh.ts | 1 + .../src/views/host/firewall/forward/index.vue | 23 ++++++- .../host/firewall/forward/operate/index.vue | 27 +++++++- 23 files changed, 109 insertions(+), 40 deletions(-) diff --git a/agent/app/dto/firewall.go b/agent/app/dto/firewall.go index 20955df7f..99920d929 100644 --- a/agent/app/dto/firewall.go +++ b/agent/app/dto/firewall.go @@ -37,6 +37,7 @@ type ForwardRuleOperate struct { Operation string `json:"operation" validate:"required,oneof=add remove"` Num string `json:"num"` Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"` + Interface string `json:"interface"` Port string `json:"port" validate:"required"` TargetIP string `json:"targetIP"` TargetPort string `json:"targetPort" validate:"required"` diff --git a/agent/app/model/firewall.go b/agent/app/model/firewall.go index 1bce122e8..622203d05 100644 --- a/agent/app/model/firewall.go +++ b/agent/app/model/firewall.go @@ -18,4 +18,5 @@ type Forward struct { Port string `gorm:"not null" json:"port"` TargetIP string `gorm:"not null" json:"targetIP"` TargetPort string `gorm:"not null" json:"targetPort"` + Interface string `json:"interface"` } diff --git a/agent/app/service/firewall.go b/agent/app/service/firewall.go index 4a8bd9df4..0bd48b33b 100644 --- a/agent/app/service/firewall.go +++ b/agent/app/service/firewall.go @@ -329,7 +329,8 @@ func (u *FirewallService) OperateForwardRule(req dto.ForwardRuleOperate) error { if reqRule.Port == rule.Port && reqRule.TargetPort == rule.TargetPort && reqRule.TargetIP == rule.TargetIP && - proto == rule.Protocol { + proto == rule.Protocol && + reqRule.Interface == rule.Interface { shouldKeep = false break } @@ -353,7 +354,8 @@ func (u *FirewallService) OperateForwardRule(req dto.ForwardRuleOperate) error { if reqRule.Port == rule.Port && reqRule.TargetPort == rule.TargetPort && reqRule.TargetIP == rule.TargetIP && - proto == rule.Protocol { + proto == rule.Protocol && + reqRule.Interface == rule.Interface { return buserr.New("ErrRecordExist") } } @@ -383,6 +385,7 @@ func (u *FirewallService) OperateForwardRule(req dto.ForwardRuleOperate) error { Port: r.Port, TargetIP: r.TargetIP, TargetPort: r.TargetPort, + Interface: r.Interface, }, r.Operation); err != nil { if req.ForceDelete { global.LOG.Error(err) diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 6f7dbf0e6..5bc862399 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -26,7 +26,7 @@ import ( ) var AddTable = &gormigrate.Migration{ - ID: "20250902-add-table", + ID: "20250930-add-table", Migrate: func(tx *gorm.DB) error { return tx.AutoMigrate( &model.AppDetail{}, diff --git a/agent/utils/firewall/client/info.go b/agent/utils/firewall/client/info.go index f88bd155b..ba1603738 100644 --- a/agent/utils/firewall/client/info.go +++ b/agent/utils/firewall/client/info.go @@ -10,6 +10,7 @@ type FireInfo struct { Num string `json:"num"` TargetIP string `json:"targetIP"` TargetPort string `json:"targetPort"` + Interface string `json:"interface"` UsedStatus string `json:"usedStatus"` Description string `json:"description"` @@ -21,12 +22,15 @@ type Forward struct { Port string `json:"port"` TargetIP string `json:"targetIP"` TargetPort string `json:"targetPort"` + Interface string `json:"interface"` } type IptablesNatInfo struct { Num string `json:"num"` Target string `json:"target"` Protocol string `json:"protocol"` + InIface string `json:"inIface"` + OutIface string `json:"outIface"` Opt string `json:"opt"` Source string `json:"source"` Destination string `json:"destination"` diff --git a/agent/utils/firewall/client/iptables.go b/agent/utils/firewall/client/iptables.go index 8e15d3a3f..f8a0bb603 100644 --- a/agent/utils/firewall/client/iptables.go +++ b/agent/utils/firewall/client/iptables.go @@ -25,7 +25,7 @@ const ( const NatChain = "1PANEL" var ( - natListRegex = regexp.MustCompile(`^(\d+)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)(?:\s+(.+?) .+?:(\d{1,5}(?::\d+)?).+?[ :](.+-.+|(?:.+:)?\d{1,5}(?:-\d{1,5})?))?$`) + natListRegex = regexp.MustCompile(`^(\d+)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)\s+(.+?)(?:\s+(.+?) .+?:(\d{1,5}(?::\d+)?).+?[ :](.+-.+|(?:.+:)?\d{1,5}(?:-\d{1,5})?))?$`) ) type Iptables struct { @@ -92,7 +92,7 @@ func (iptables *Iptables) NatList(chain ...string) ([]IptablesNatInfo, error) { if len(chain) == 0 { chain = append(chain, PreRoutingChain) } - stdout, err := iptables.outf(NatTab, "-nL %s --line", chain[0]) + stdout, err := iptables.outf(NatTab, "-nvL %s --line-numbers", chain[0]) if err != nil { return nil, err } @@ -104,18 +104,20 @@ func (iptables *Iptables) NatList(chain ...string) ([]IptablesNatInfo, error) { }) if natListRegex.MatchString(line) { match := natListRegex.FindStringSubmatch(line) - if !strings.Contains(match[9], ":") { - match[9] = fmt.Sprintf(":%s", match[9]) + if !strings.Contains(match[13], ":") { + match[13] = fmt.Sprintf(":%s", match[13]) } forwardList = append(forwardList, IptablesNatInfo{ Num: match[1], - Target: match[2], - Protocol: match[7], - Opt: match[4], - Source: match[5], - Destination: match[6], - SrcPort: match[8], - DestPort: match[9], + Target: match[4], + Protocol: match[11], + InIface: match[7], + OutIface: match[8], + Opt: match[6], + Source: match[9], + Destination: match[10], + SrcPort: match[12], + DestPort: match[13], }) } } @@ -123,16 +125,14 @@ func (iptables *Iptables) NatList(chain ...string) ([]IptablesNatInfo, error) { return forwardList, nil } -func (iptables *Iptables) NatAdd(protocol, srcPort, dest, destPort string, save bool) error { +func (iptables *Iptables) NatAdd(protocol, srcPort, dest, destPort, iface string, save bool) error { if dest != "" && dest != "127.0.0.1" && dest != "localhost" { - if err := iptables.runf(NatTab, fmt.Sprintf( - "-A %s -p %s --dport %s -j DNAT --to-destination %s:%s", - PreRoutingChain, - protocol, - srcPort, - dest, - destPort, - )); err != nil { + iptablesArg := fmt.Sprintf("-A %s", PreRoutingChain) + if iface != "" { + iptablesArg += fmt.Sprintf(" -i %s", iface) + } + iptablesArg += fmt.Sprintf(" -p %s --dport %s -j DNAT --to-destination %s:%s", protocol, srcPort, dest, destPort) + if err := iptables.runf(NatTab, iptablesArg); err != nil { return err } @@ -166,13 +166,12 @@ func (iptables *Iptables) NatAdd(protocol, srcPort, dest, destPort string, save return err } } else { - if err := iptables.runf(NatTab, fmt.Sprintf( - "-A %s -p %s --dport %s -j REDIRECT --to-port %s", - PreRoutingChain, - protocol, - srcPort, - destPort, - )); err != nil { + iptablesArg := fmt.Sprintf("-A %s", PreRoutingChain) + if iface != "" { + iptablesArg += fmt.Sprintf(" -i %s", iface) + } + iptablesArg += fmt.Sprintf(" -p %s --dport %s -j REDIRECT --to-port %s", protocol, srcPort, destPort) + if err := iptables.runf(NatTab, iptablesArg); err != nil { return err } } @@ -183,12 +182,13 @@ func (iptables *Iptables) NatAdd(protocol, srcPort, dest, destPort string, save Port: srcPort, TargetIP: dest, TargetPort: destPort, + Interface: iface, }).Error } return nil } -func (iptables *Iptables) NatRemove(num string, protocol, srcPort, dest, destPort string) error { +func (iptables *Iptables) NatRemove(num string, protocol, srcPort, dest, destPort, iface string) error { if err := iptables.runf(NatTab, "-D %s %s", PreRoutingChain, num); err != nil { return err } @@ -226,11 +226,13 @@ func (iptables *Iptables) NatRemove(num string, protocol, srcPort, dest, destPor } global.DB.Where( - "protocol = ? AND port = ? AND target_ip = ? AND target_port = ?", + "protocol = ? AND port = ? AND target_ip = ? AND target_port = ? AND (interface = ? OR (interface IS NULL AND ? = ''))", protocol, srcPort, dest, destPort, + iface, + iface, ).Delete(&model.Forward{}) return nil } @@ -249,7 +251,7 @@ func (iptables *Iptables) Reload() error { var rules []model.Forward global.DB.Find(&rules) for _, forward := range rules { - if err := iptables.NatAdd(forward.Protocol, forward.Port, forward.TargetIP, forward.TargetPort, false); err != nil { + if err := iptables.NatAdd(forward.Protocol, forward.Port, forward.TargetIP, forward.TargetPort, forward.Interface, false); err != nil { return err } } diff --git a/agent/utils/firewall/client/ufw.go b/agent/utils/firewall/client/ufw.go index acaf74317..883e58098 100644 --- a/agent/utils/firewall/client/ufw.go +++ b/agent/utils/firewall/client/ufw.go @@ -124,6 +124,7 @@ func (f *Ufw) ListForward() ([]FireInfo, error) { list = append(list, FireInfo{ Num: rule.Num, Protocol: rule.Protocol, + Interface: rule.InIface, Port: rule.SrcPort, TargetIP: dest[0], TargetPort: dest[1], @@ -241,9 +242,9 @@ func (f *Ufw) PortForward(info Forward, operation string) error { } if operation == "add" { - err = iptables.NatAdd(info.Protocol, info.Port, info.TargetIP, info.TargetPort, true) + err = iptables.NatAdd(info.Protocol, info.Port, info.TargetIP, info.TargetPort, info.Interface, true) } else { - err = iptables.NatRemove(info.Num, info.Protocol, info.Port, info.TargetIP, info.TargetPort) + err = iptables.NatRemove(info.Num, info.Protocol, info.Port, info.TargetIP, info.TargetPort, info.Interface) } if err != nil { return fmt.Errorf("%s port forward failed, err: %s", operation, err) diff --git a/frontend/src/api/interface/dashboard.ts b/frontend/src/api/interface/dashboard.ts index d4d22e733..abb640740 100644 --- a/frontend/src/api/interface/dashboard.ts +++ b/frontend/src/api/interface/dashboard.ts @@ -15,7 +15,7 @@ export namespace Dashboard { title: string; detail: string; recommend: number; - isShow: boolean ; + isShow: boolean; router: string; } export interface AppLauncher { diff --git a/frontend/src/api/interface/host.ts b/frontend/src/api/interface/host.ts index 4baabfb2a..0bb5ecf15 100644 --- a/frontend/src/api/interface/host.ts +++ b/frontend/src/api/interface/host.ts @@ -112,6 +112,7 @@ export namespace Host { port: string; targetIP: string; targetPort: string; + interface: string; } export interface RuleIP { operation: string; diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 3c930a9e5..a82cf7e9e 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -246,7 +246,7 @@ export namespace Setting { id: number; name: string; addr: string; - description: string; + description: string; systemVersion: string; securityEntrance: string; cpuUsedPercent: number; diff --git a/frontend/src/api/modules/host.ts b/frontend/src/api/modules/host.ts index 82aa55a16..4fc6da273 100644 --- a/frontend/src/api/modules/host.ts +++ b/frontend/src/api/modules/host.ts @@ -25,7 +25,7 @@ export const operateFire = (operation: string, withDockerRestart: boolean) => { export const operatePortRule = (params: Host.RulePort) => { return http.post(`/hosts/firewall/port`, params, TimeoutEnum.T_40S); }; -export const operateForwardRule = (params: { rules: Host.RuleForward[]; forceDelete: boolean }) => { +export const operateForwardRule = (params: { rules: Host.RuleForward[]; forceDelete?: boolean }) => { return http.post(`/hosts/firewall/forward`, params, TimeoutEnum.T_40S); }; export const operateIPRule = (params: Host.RuleIP) => { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 647dbd9b9..488991725 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -2857,6 +2857,7 @@ const message = { forwardHelper1: 'If you want to forward to the local port, the destination IP should be set to "127.0.0.1".', forwardHelper2: 'Leave the destination IP blank to forward to the local port.', forwardHelper3: 'Only support IPv4 port forwarding.', + forwardInboundInterface: 'Forward Inbound Network Interface', }, runtime: { runtime: 'Runtime', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index e14b0176b..3e32ba47e 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -2824,6 +2824,7 @@ const message = { forwardHelper1: 'Si quieres reenviar al puerto local, la IP de destino debe ser "127.0.0.1".', forwardHelper2: 'Deja en blanco la IP de destino para reenviar al puerto local.', forwardHelper3: 'Solo se admite redirección de puertos IPv4.', + forwardInboundInterface: 'Interfaz de Red de Entrada para Reenvío', }, runtime: { runtime: 'Runtime', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 5a6b37157..d588af3d2 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -2771,6 +2771,7 @@ const message = { forwardHelper1: 'ローカルポートに転送する場合は、宛先IPを「127.0.0.1」に設定する必要があります。', forwardHelper2: '宛先IPを空白のままにして、ローカルポートに転送します。', forwardHelper3: 'IPv4ポート転送のみをサポートします。', + forwardInboundInterface: '転送入站ネットワークインターフェース', }, runtime: { runtime: 'ランタイム', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index d10c733e7..7677fc9c6 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -2722,6 +2722,7 @@ const message = { forwardHelper1: "로컬 포트로 전달하려면, 대상 IP 를 '127.0.0.1'로 설정해야 합니다.", forwardHelper2: '대상 IP 를 비워두면 로컬 포트로 전달됩니다.', forwardHelper3: 'IPv4 포트 전달만 지원됩니다.', + forwardInboundInterface: '포워딩 인바운드 네트워크 인터페이스', }, runtime: { runtime: '실행 환경', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index aa27293f7..746fd2be9 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -2833,6 +2833,7 @@ const message = { forwardHelper1: 'Jika anda ingin memajukan ke port tempatan, IP sasaran harus ditetapkan kepada "127.0.0.1".', forwardHelper2: 'Biarkan IP sasaran kosong untuk memajukan ke port tempatan.', forwardHelper3: 'Hanya menyokong pemajuan port IPv4.', + forwardInboundInterface: 'Antara Muka Rangkaian Masukan Penerusan', }, runtime: { runtime: 'Runtime', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 4f0bd341a..e53fe3ace 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -2838,6 +2838,7 @@ const message = { 'Se você deseja redirecionar para a porta local, o IP de destino deve ser definido como "127.0.0.1".', forwardHelper2: 'Deixe o IP de destino em branco para redirecionar para a porta local.', forwardHelper3: 'Somente suporta redirecionamento de porta IPv4.', + forwardInboundInterface: 'Interface de Rede de Entrada para Encaminhamento', }, runtime: { runtime: 'Runtime', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 154cb7c0c..f2298dd1b 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -2834,6 +2834,7 @@ const message = { 'Если вы хотите перенаправить на локальный порт, целевой IP должен быть установлен как "127.0.0.1".', forwardHelper2: 'Оставьте целевой IP пустым для перенаправления на локальный порт.', forwardHelper3: 'Поддерживается только переадресация портов IPv4.', + forwardInboundInterface: '转发入站Сетевой интерфейс для пересылки входящего трафика网卡', }, runtime: { runtime: 'Среда выполнения', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 66b75b417..3ff274dde 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -2895,6 +2895,7 @@ const message = { forwardHelper1: 'Yerel porta yönlendirmek istiyorsanız, hedef IP "127.0.0.1" olarak ayarlanmalıdır.', forwardHelper2: 'Yerel porta yönlendirmek için hedef IP’yi boş bırakın.', forwardHelper3: 'Yalnızca IPv4 port yönlendirmesini destekler.', + forwardInboundInterface: 'İletme Gelen Ağ Arayüzü', }, runtime: { runtime: 'Çalışma Zamanı', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index 793a8c1fd..4a7082eba 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -2659,6 +2659,7 @@ const message = { forwardHelper1: '如果是本機埠轉發,目標 IP 為:127.0.0.1', forwardHelper2: '如果目標 IP 不填寫,預設為本機埠轉發', forwardHelper3: '目前僅支援 IPv4 的埠轉發', + forwardInboundInterface: '轉發入站網路介面', }, runtime: { runtime: '執行環境', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index bff1ce514..c146c8f12 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -2651,6 +2651,7 @@ const message = { forwardHelper1: '如果是本机端口转发,目标IP为:127.0.0.1', forwardHelper2: '如果目标IP不填写,则默认为本机端口转发', forwardHelper3: '当前仅支持 IPv4 的端口转发', + forwardInboundInterface: '转发入站网卡', }, runtime: { runtime: '运行环境', diff --git a/frontend/src/views/host/firewall/forward/index.vue b/frontend/src/views/host/firewall/forward/index.vue index e0e54b6ff..a5bdca384 100644 --- a/frontend/src/views/host/firewall/forward/index.vue +++ b/frontend/src/views/host/firewall/forward/index.vue @@ -43,6 +43,19 @@ + { await searchFireRule(params) .then((res) => { loading.value = false; - data.value = res.data.items || []; + data.value = + res.data.items?.map((item) => { + return { + ...item, + interface: item.interface === '*' ? '' : item.interface, + }; + }) || []; paginationConfig.total = res.data.total; }) .catch(() => { @@ -141,11 +160,13 @@ const onOpenDialog = async ( port: '8080', targetIP: '', targetPort: '', + interface: '', }, ) => { let params = { title, rowData: { ...rowData }, + fireName: fireName.value, }; dialogRef.value!.acceptParams(params); }; diff --git a/frontend/src/views/host/firewall/forward/operate/index.vue b/frontend/src/views/host/firewall/forward/operate/index.vue index e70cee054..cbaa5da93 100644 --- a/frontend/src/views/host/firewall/forward/operate/index.vue +++ b/frontend/src/views/host/firewall/forward/operate/index.vue @@ -23,6 +23,19 @@ + +