fix: support trusted proxies for allowed IPs (#13608)

This commit is contained in:
ssongliu
2026-08-21 17:33:14 +08:00
committed by GitHub
parent 825221b2bb
commit 6f6747a584
24 changed files with 224 additions and 138 deletions

View File

@@ -108,6 +108,14 @@ func (b *BaseApi) UpdateSetting(c *gin.Context) {
}
req.Value = value
}
if req.Key == "AllowIPTrustedProxies" {
value, err := common.NormalizeTrustedProxies(req.Value)
if err != nil {
helper.BadRequest(c, err)
return
}
req.Value = value
}
if err := settingService.Update(c, req.Key, req.Value); err != nil {
helper.InternalServer(c, err)

View File

@@ -6,7 +6,6 @@ import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net"
"strconv"
"strings"
@@ -108,120 +107,11 @@ func LoadAPIAuthConfig(_ *gin.Context) (APIAuthConfig, error) {
}
func GetAPIClientIP(c *gin.Context, trustedProxies string) string {
remoteAddr := common.GetRealClientIP(c)
remoteIP := net.ParseIP(remoteAddr)
if remoteIP == nil {
return remoteAddr
}
proxies, err := parseAPITrustedProxies(trustedProxies)
if err != nil {
if global.LOG != nil {
global.LOG.Errorf("Failed to parse API trusted proxies: %v", err)
}
return remoteAddr
}
if !isIPInNetworks(remoteIP, proxies) {
return remoteAddr
}
forwardedFor := strings.Join(c.Request.Header.Values("X-Forwarded-For"), ",")
if strings.TrimSpace(forwardedFor) != "" {
clientIP, ok := clientIPFromForwardedFor(forwardedFor, proxies)
if !ok {
return remoteAddr
}
return clientIP
}
realIPValue := strings.Join(c.Request.Header.Values("X-Real-IP"), ",")
realIP := net.ParseIP(strings.TrimSpace(realIPValue))
if realIP == nil {
return remoteAddr
}
return realIP.String()
return common.ResolveClientIP(c, trustedProxies)
}
func NormalizeAPITrustedProxies(value string) (string, error) {
lines := strings.Split(value, "\n")
normalized := make([]string, 0, len(lines))
for _, line := range lines {
item := strings.TrimSpace(line)
if item == "" {
continue
}
if ip := net.ParseIP(item); ip != nil {
normalized = append(normalized, ip.String())
continue
}
_, ipNet, err := net.ParseCIDR(item)
if err != nil {
return "", fmt.Errorf("invalid API trusted proxy entry %q: %w", item, err)
}
ones, _ := ipNet.Mask.Size()
if ones == 0 {
return "", fmt.Errorf("invalid API trusted proxy entry %q: unrestricted CIDR is not allowed", item)
}
normalized = append(normalized, ipNet.String())
}
return strings.Join(normalized, "\n"), nil
}
func parseAPITrustedProxies(value string) ([]*net.IPNet, error) {
normalized, err := NormalizeAPITrustedProxies(value)
if err != nil {
return nil, err
}
if normalized == "" {
return []*net.IPNet{}, nil
}
lines := strings.Split(normalized, "\n")
proxies := make([]*net.IPNet, 0, len(lines))
for _, item := range lines {
if ip := net.ParseIP(item); ip != nil {
bits := 128
if ip.To4() != nil {
bits = 32
ip = ip.To4()
}
proxies = append(proxies, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
continue
}
_, ipNet, err := net.ParseCIDR(item)
if err != nil {
return nil, err
}
proxies = append(proxies, ipNet)
}
return proxies, nil
}
func clientIPFromForwardedFor(value string, trustedProxies []*net.IPNet) (string, bool) {
items := strings.Split(value, ",")
ips := make([]net.IP, len(items))
for i, item := range items {
ip := net.ParseIP(strings.TrimSpace(item))
if ip == nil {
return "", false
}
ips[i] = ip
}
for i := len(ips) - 1; i >= 0; i-- {
if i == 0 || !isIPInNetworks(ips[i], trustedProxies) {
return ips[i].String(), true
}
}
return "", false
}
func isIPInNetworks(ip net.IP, networks []*net.IPNet) bool {
for _, network := range networks {
if network.Contains(ip) {
return true
}
}
return false
return common.NormalizeTrustedProxies(value)
}
func IsValid1PanelTimestamp(panelTimestamp string, apiKeyValidityTime int) bool {

View File

@@ -27,6 +27,7 @@ type SettingInfo struct {
BindDomain string `json:"bindDomain"`
PasskeyTrustedProxies string `json:"passkeyTrustedProxies"`
AllowIPs string `json:"allowIPs"`
AllowIPTrustedProxies string `json:"allowIPTrustedProxies"`
SecurityEntrance string `json:"securityEntrance"`
DashboardMemoVisible string `json:"dashboardMemoVisible"`
DashboardSimpleNodeVisible string `json:"dashboardSimpleNodeVisible"`

View File

@@ -52,6 +52,7 @@ func Init() {
migrations.AddAlertAuditUser,
migrations.AddMenuAccordionSetting,
migrations.AddAPITrustedProxiesSetting,
migrations.AddAllowIPTrustedProxiesSetting,
migrations.AddWebsiteTemplateMenu,
migrations.RepairXpackAppMenus,
})

View File

@@ -172,6 +172,9 @@ var InitSetting = &gormigrate.Migration{
if err := tx.Create(&model.Setting{Key: "AllowIPs", Value: ""}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "AllowIPTrustedProxies", Value: ""}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "NoAuthSetting", Value: "200"}).Error; err != nil {
return err
}
@@ -1301,6 +1304,20 @@ var AddAPITrustedProxiesSetting = &gormigrate.Migration{
},
}
var AddAllowIPTrustedProxiesSetting = &gormigrate.Migration{
ID: "20260820-add-allow-ip-trusted-proxies-setting",
Migrate: func(tx *gorm.DB) error {
var setting model.Setting
if err := tx.Where("key = ?", "AllowIPTrustedProxies").First(&setting).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return tx.Create(&model.Setting{Key: "AllowIPTrustedProxies", Value: ""}).Error
}
return err
}
return nil
},
}
var AddWebsiteTemplateMenu = &gormigrate.Migration{
ID: "20260728-add-website-template-menu",
Migrate: func(tx *gorm.DB) error {

View File

@@ -45,6 +45,7 @@ var baseSettingKeys = map[string]struct{}{
"SecurityEntrance": {},
"BindDomain": {},
"AllowIPs": {},
"AllowIPTrustedProxies": {},
"PasskeyTrustedProxies": {},
"ComplexityVerification": {},
"NoAuthSetting": {},

View File

@@ -14,18 +14,21 @@ import (
func WhiteAllow() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("X-Panel-Local-Token")
clientIP := common.GetRealClientIP(c)
if isLocalSyncRequest(c.Request.URL.Path, clientIP, tokenString) {
remoteIP := common.GetRealClientIP(c)
if isLocalSyncRequest(c.Request.URL.Path, remoteIP, tokenString) {
c.Set("LOCAL_REQUEST", true)
c.Next()
return
}
if common.IsPrivateIP(clientIP) {
c.Next()
return
}
settingRepo := repo.NewISettingRepo()
trustedProxies, err := settingRepo.GetValueByKey("AllowIPTrustedProxies")
if err != nil {
helper.InternalServer(c, err)
return
}
clientIP := common.ResolveClientIP(c, trustedProxies)
allowIPs, err := settingRepo.GetValueByKey("AllowIPs")
if err != nil {
helper.InternalServer(c, err)

View File

@@ -0,0 +1,122 @@
package common
import (
"fmt"
"net"
"strings"
"github.com/gin-gonic/gin"
)
// ResolveClientIP returns the TCP peer address unless that peer is trusted.
// Forwarded client address headers are never used for untrusted peers.
func ResolveClientIP(c *gin.Context, trustedProxies string) string {
remoteAddr := GetRealClientIP(c)
remoteIP := net.ParseIP(remoteAddr)
if remoteIP == nil {
return remoteAddr
}
proxies, err := parseTrustedProxies(trustedProxies)
if err != nil || !isIPInNetworks(remoteIP, proxies) {
return remoteAddr
}
forwardedFor := strings.Join(c.Request.Header.Values("X-Forwarded-For"), ",")
if strings.TrimSpace(forwardedFor) != "" {
clientIP, ok := clientIPFromForwardedFor(forwardedFor, proxies)
if !ok {
return remoteAddr
}
return clientIP
}
realIPValue := strings.Join(c.Request.Header.Values("X-Real-IP"), ",")
realIP := net.ParseIP(strings.TrimSpace(realIPValue))
if realIP == nil {
return remoteAddr
}
return realIP.String()
}
func NormalizeTrustedProxies(value string) (string, error) {
lines := strings.Split(value, "\n")
normalized := make([]string, 0, len(lines))
for _, line := range lines {
item := strings.TrimSpace(line)
if item == "" {
continue
}
if ip := net.ParseIP(item); ip != nil {
normalized = append(normalized, ip.String())
continue
}
_, ipNet, err := net.ParseCIDR(item)
if err != nil {
return "", fmt.Errorf("invalid trusted proxy entry %q: %w", item, err)
}
ones, _ := ipNet.Mask.Size()
if ones == 0 {
return "", fmt.Errorf("invalid trusted proxy entry %q: unrestricted CIDR is not allowed", item)
}
normalized = append(normalized, ipNet.String())
}
return strings.Join(normalized, "\n"), nil
}
func parseTrustedProxies(value string) ([]*net.IPNet, error) {
normalized, err := NormalizeTrustedProxies(value)
if err != nil {
return nil, err
}
if normalized == "" {
return []*net.IPNet{}, nil
}
lines := strings.Split(normalized, "\n")
proxies := make([]*net.IPNet, 0, len(lines))
for _, item := range lines {
if ip := net.ParseIP(item); ip != nil {
bits := 128
if ip.To4() != nil {
bits = 32
ip = ip.To4()
}
proxies = append(proxies, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
continue
}
_, ipNet, err := net.ParseCIDR(item)
if err != nil {
return nil, err
}
proxies = append(proxies, ipNet)
}
return proxies, nil
}
func clientIPFromForwardedFor(value string, trustedProxies []*net.IPNet) (string, bool) {
items := strings.Split(value, ",")
ips := make([]net.IP, len(items))
for i, item := range items {
ip := net.ParseIP(strings.TrimSpace(item))
if ip == nil {
return "", false
}
ips[i] = ip
}
for i := len(ips) - 1; i >= 0; i-- {
if i == 0 || !isIPInNetworks(ips[i], trustedProxies) {
return ips[i].String(), true
}
}
return "", false
}
func isIPInNetworks(ip net.IP, networks []*net.IPNet) bool {
for _, network := range networks {
if network.Contains(ip) {
return true
}
}
return false
}

View File

@@ -195,10 +195,8 @@ func checkIPLimit(c *gin.Context) bool {
if len(status.Value) == 0 {
return true
}
clientIP := common.GetRealClientIP(c)
if common.IsPrivateIP(clientIP) {
return true
}
trustedProxies, _ := settingRepo.Get(repo.WithByKey("AllowIPTrustedProxies"))
clientIP := common.ResolveClientIP(c, trustedProxies.Value)
for _, ip := range strings.Split(status.Value, ",") {
if len(ip) == 0 {

View File

@@ -50,6 +50,7 @@ export namespace Setting {
ssl: string;
sslType: string;
allowIPs: string;
allowIPTrustedProxies: string;
bindDomain: string;
passkeyTrustedProxies: string;
securityEntrance: string;

View File

@@ -2823,7 +2823,7 @@ const message = {
apiTrustedProxies: 'API trusted proxies',
apiTrustedProxiesEgs: 'One proxy server IP or CIDR per line. For example,\n172.16.10.111\n172.16.10.0/24',
apiTrustedProxiesHelper:
'When using a reverse proxy, enter the IP address or network of the proxy server to correctly obtain the client IP. Otherwise, leave this blank. 0.0.0.0/0 and ::/0 are not supported.',
'When using a reverse proxy, enter its IP address or network. When empty, no proxy is trusted and the direct source IP is used. 0.0.0.0/0 and ::/0 are not supported.',
apiKeyValidityTime: 'Validity period of interface key',
apiKeyValidityTimeEgs: 'Validity period of interface key (in minutes)',
apiKeyValidityTimeHelper:
@@ -3025,6 +3025,8 @@ const message = {
allowIPsWarning:
'After you set the authorized IP address list, only the IP address in the list can access the panel service. Continue?',
allowIPsHelper1: 'Leave it blank to disable the IP address restriction.',
allowIPsPrivateHelper:
'Private-network addresses are also restricted. For local access, add 127.0.0.1 or ::1; for reverse proxies, configure trusted proxies.',
allowIPEgs: 'One per line. For example,\n172.16.10.111\n172.16.10.0/24',
mfa: 'Two-Factor Auth',
mfaClose: 'Disabling MFA will reduce the security of the service. Continue?',

View File

@@ -2870,7 +2870,7 @@ const message = {
apiTrustedProxies: 'Proxies de confianza de la API',
apiTrustedProxiesEgs: 'Una IP o CIDR del servidor proxy por línea. Por ejemplo,\n172.16.10.111\n172.16.10.0/24',
apiTrustedProxiesHelper:
'Al usar un proxy inverso, introduce la IP o la red del servidor proxy para obtener correctamente la IP del cliente. Si no lo usas, deja este campo vacío. No se admiten 0.0.0.0/0 ni ::/0.',
'Al usar un proxy inverso, introduce su IP o red. Si se deja vacío, no se confía en ningún proxy y se usa la IP de origen directa. No se admiten 0.0.0.0/0 ni ::/0.',
apiKeyValidityTime: 'Periodo de validez de la clave de interfaz',
apiKeyValidityTimeEgs: 'Periodo de validez de la clave de interfaz (en minutos)',
apiKeyValidityTimeHelper:
@@ -3079,6 +3079,8 @@ const message = {
allowIPsWarning:
'Tras configurar la lista de IP autorizadas, solo las IP en la lista podrán acceder al panel. ¿Deseas continuar?',
allowIPsHelper1: 'Déjalo en blanco para desactivar la restricción por IP.',
allowIPsPrivateHelper:
'Las direcciones de red privada también están restringidas. Para acceso local, añade 127.0.0.1 o ::1; para proxies inversos, configura proxies de confianza.',
allowIPEgs: 'Una por línea. Por ejemplo,\n172.16.10.111\n172.16.10.0/24',
mfa: 'Autenticación en Dos Pasos',
mfaClose: 'Desactivar MFA reducirá la seguridad del servicio. ¿Deseas continuar?',

View File

@@ -2794,7 +2794,7 @@ const message = {
apiTrustedProxies: 'پروکسی‌های قابل اعتماد API',
apiTrustedProxiesEgs: 'در هر خط یک IP یا CIDR سرور پروکسی. مثلاً،\n172.16.10.111\n172.16.10.0/24',
apiTrustedProxiesHelper:
'هنگام استفاده از پروکسی معکوس، IP یا شبکه سرور پروکسی را وارد کنید تا IP کلاینت به‌درستی دریافت شود. در صورت عدم استفاده، این بخش را خالی بگذارید. 0.0.0.0/0 و ::/0 پشتیبانی نمی‌شوند.',
'هنگام استفاده از پروکسی معکوس، IP یا شبکه آن را وارد کنید. اگر خالی باشد، هیچ پروکسی قابل اعتماد نیست و IP مبدأ مستقیم استفاده می‌شود. 0.0.0.0/0 و ::/0 پشتیبانی نمی‌شوند.',
apiKeyValidityTime: 'مدت اعتبار کلید رابط',
apiKeyValidityTimeEgs: 'مدت اعتبار کلید رابط (به دقیقه)',
apiKeyValidityTimeHelper:
@@ -2993,6 +2993,8 @@ const message = {
allowIPsWarning:
'پس از تنظیم لیست آدرس‌های IP مجاز، فقط آدرس IP موجود در لیست می‌تواند به سرویس پنل دسترسی داشته باشد. ادامه می‌دهید؟',
allowIPsHelper1: 'خالی بگذارید تا محدودیت IP غیرفعال شود.',
allowIPsPrivateHelper:
'آدرس‌های شبکه خصوصی نیز محدود می‌شوند. برای دسترسی محلی 127.0.0.1 یا ::1 را اضافه کنید؛ برای پروکسی معکوس، پروکسی قابل اعتماد را تنظیم کنید.',
allowIPEgs: 'هر خط یک IP. مثلاً،\n172.16.10.111\n172.16.10.0/24',
mfa: 'احراز هویت دو مرحله‌ای',
mfaClose: 'غیرفعال‌سازی MFA امنیت سرویس را کاهش می‌دهد. ادامه می‌دهید؟',

View File

@@ -2802,7 +2802,7 @@ const message = {
apiTrustedProxies: 'API信頼済みプロキシ',
apiTrustedProxiesEgs: 'プロキシサーバーのIPまたはCIDRを1行に1つ入力します\n172.16.10.111\n172.16.10.0/24',
apiTrustedProxiesHelper:
'リバースプロキシを使用する場合はクライアントIPを正しく取得するためにプロキシサーバーのIPまたはネットワークを入力してください使用しない場合は空欄にしてください0.0.0.0/0::/0は指定できません',
'リバースプロキシを使用する場合はその IP またはネットワークを入力してください空欄の場合はどのプロキシも信頼せず直接の接続元 IP を使用します0.0.0.0/0 ::/0 は指定できません',
apiKeyReset: 'インターフェイスキーリセット',
apiKeyResetHelper: '関連するキーサービスは無効になりますサービスに新しいキーを追加してください',
confDockerProxy: 'Dockerプロキシを構成します',
@@ -2986,6 +2986,8 @@ const message = {
allowIPsWarning:
'承認されたIPアドレスリストを設定した後リスト内のIPアドレスのみがパネルサービスにアクセスできます続けたいですか',
allowIPsHelper1: 'IPアドレスの制限を無効にするために空白のままにします',
allowIPsPrivateHelper:
'プライベートネットワークのアドレスも制限されますローカル接続には 127.0.0.1 または ::1 を追加しリバースプロキシには信頼済みプロキシを設定してください',
allowIPEgs: '1行に1つたとえば \n172.16.10.111 \n172.16.10.0/24',
mfa: '二段階認証',
mfaClose: 'MFAを無効にするとサービスのセキュリティが減少します続けたいですか',

View File

@@ -2763,7 +2763,7 @@ const message = {
apiTrustedProxiesEgs:
'프록시 서버 IP 또는 CIDR을 한 줄에 하나씩 입력하십시오. 예:\n172.16.10.111\n172.16.10.0/24',
apiTrustedProxiesHelper:
'리버스 프록시를 사용하는 경우 클라이언트 IP를 올바르게 가져오려면 프록시 서버의 IP 또는 네트워크를 입력하세요. 사용하지 않는 경우 비워 두세요. 0.0.0.0/0 및 ::/0은 지원되지 않습니다.',
'리버스 프록시를 사용하는 경우 해당 IP 또는 네트워크를 입력하세요. 비워 두면 어떤 프록시도 신뢰하지 않고 직접 연결된 출발지 IP를 사용합니다. 0.0.0.0/0 및 ::/0은 지원되지 않습니다.',
apiKeyValidityTime: '인터페이스 키 유효 기간',
apiKeyValidityTimeEgs: '인터페이스 키 유효 기간 (분 단위)',
apiKeyValidityTimeHelper:
@@ -2938,6 +2938,8 @@ const message = {
allowIPsWarning:
'허가된 IP 주소 목록을 설정하면 목록에 있는 IP 주소만 패널 서비스에 접근할 수 있습니다. 계속하시겠습니까?',
allowIPsHelper1: '공백으로 남기면 IP 주소 제한이 비활성화됩니다.',
allowIPsPrivateHelper:
'사설 네트워크 주소도 제한됩니다. 로컬 접속에는 127.0.0.1 또는 ::1을 추가하고, 리버스 프록시에는 신뢰할 수 있는 프록시를 설정하세요.',
allowIPEgs: '한 줄에 하나씩 입력하십시오. 예:\n172.16.10.111\n172.16.10.0/24',
mfa: '2단계 인증',
mfaClose: 'MFA를 비활성화하면 서비스 보안이 낮아집니다. 계속하시겠습니까?',

View File

@@ -2746,7 +2746,7 @@ const message = {
apiTrustedProxies: 'ພຣັອກຊີ API ທີ່ເຊື່ອຖື',
apiTrustedProxiesEgs: 'ໜຶ່ງ IP ຫຼື CIDR ຂອງ proxy server ຕໍ່ແຖວ. ຕົວຢ່າງ:\n172.16.10.111\n172.16.10.0/24',
apiTrustedProxiesHelper:
'ເມື່ອໃຊ້ reverse proxy, ໃຫ້ໃສ່ IP ຫຼືເຄືອຂ່າຍຂອງ proxy server ເພື່ອໃຫ້ໄດ້ client IP ຢ່າງຖືກຕ້ອງ. ຖ້າບໍ່ໃຊ້ໃຫ້ປ່ອຍຫວ່າງ. ບໍ່ຮອງຮັບ 0.0.0.0/0 ແລະ ::/0.',
'ເມື່ອໃຊ້ reverse proxy ໃຫ້ໃສ່ IP ຫຼືເຄືອຂ່າຍຂອງມັນ. ຖ້າປ່ອຍຫວ່າງ ຈະບໍ່ເຊື່ອຖື proxy ໃດ ແລະໃຊ້ IP ຕົ້ນທາງໂດຍກົງ. ບໍ່ຮອງຮັບ 0.0.0.0/0 ແລະ ::/0.',
apiKeyValidityTime: 'ໄລຍະເວລາທີ່ API key ໃຊ້ງານໄດ້',
apiKeyValidityTimeEgs: 'ໄລຍະເວລາທີ່ API key ໃຊ້ງານໄດ້ (ນາທີ)',
apiKeyValidityTimeHelper:
@@ -2940,6 +2940,8 @@ const message = {
allowIPsWarning:
'ຫຼັງຈາກຕັ້ງລາຍຊື່ IP ທີ່ໄດ້ຮັບອະນຸຍາດ, ຈະມີພຽງ IP ໃນລາຍຊື່ເທົ່ານັ້ນທີ່ສາມາດເຂົ້າເຖິງພາເນລໄດ້. ຕ້ອງການເຮັດຕໍ່ບໍ?',
allowIPsHelper1: 'ປະຫວ່າງໄວ້ເພື່ອປິດການຈຳກັດ IP.',
allowIPsPrivateHelper:
'ທີ່ຢູ່ເຄືອຂ່າຍສ່ວນຕົວກໍຖືກຈຳກັດ. ສຳລັບການເຂົ້າເຖິງພາຍໃນ ໃຫ້ເພີ່ມ 127.0.0.1 ຫຼື ::1; ສຳລັບ reverse proxy ໃຫ້ຕັ້ງຄ່າ proxy ທີ່ເຊື່ອຖື.',
allowIPEgs: 'ໜຶ່ງລາຍການຕໍ່ແຖວ. ຕົວຢ່າງ:\n172.16.10.111\n172.16.10.0/24',
mfa: 'ການຢືນຢັນຕົວຕົນສອງຊັ້ນ (MFA)',
mfaClose: 'ການປິດ MFA ຈະຫຼຸດຄວາມປອດໄພຂອງບໍລິການ. ຕ້ອງການເຮັດຕໍ່ບໍ?',

View File

@@ -2867,7 +2867,7 @@ const message = {
apiTrustedProxiesEgs:
'Satu IP atau CIDR pelayan proksi bagi setiap baris. Contoh:\n172.16.10.111\n172.16.10.0/24',
apiTrustedProxiesHelper:
'Apabila menggunakan proksi songsang, masukkan IP atau rangkaian pelayan proksi untuk mendapatkan IP klien dengan betul. Jika tidak digunakan, biarkan kosong. 0.0.0.0/0 dan ::/0 tidak disokong.',
'Apabila menggunakan proksi songsang, masukkan IP atau rangkaiannya. Jika kosong, tiada proksi dipercayai dan IP sumber langsung digunakan. 0.0.0.0/0 dan ::/0 tidak disokong.',
apiKeyValidityTime: 'Tempoh sah kunci antara muka',
apiKeyValidityTimeEgs: 'Tempoh sah kunci antara muka (dalam minit)',
apiKeyValidityTimeHelper:
@@ -3060,6 +3060,8 @@ const message = {
allowIPsWarning:
'Selepas anda menetapkan senarai alamat IP yang dibenarkan, hanya alamat IP dalam senarai yang boleh mengakses perkhidmatan panel. Adakah anda mahu meneruskan?',
allowIPsHelper1: 'Biarkan kosong untuk melumpuhkan sekatan alamat IP.',
allowIPsPrivateHelper:
'Alamat rangkaian peribadi juga disekat. Untuk akses setempat, tambahkan 127.0.0.1 atau ::1; untuk proksi songsang, tetapkan proksi dipercayai.',
allowIPEgs: 'Satu per baris. Contoh,\n172.16.10.111\n172.16.10.0/24',
mfa: 'Pengesahan Dua Faktor',
mfaClose: 'Melumpuhkan MFA akan mengurangkan keselamatan perkhidmatan. Adakah anda mahu meneruskan?',

View File

@@ -2868,7 +2868,7 @@ const message = {
apiTrustedProxies: 'Proxies confiáveis da API',
apiTrustedProxiesEgs: 'Um IP ou CIDR do servidor proxy por linha. Exemplo:\n172.16.10.111\n172.16.10.0/24',
apiTrustedProxiesHelper:
'Ao usar um proxy reverso, insira o IP ou a rede do servidor proxy para obter corretamente o IP do cliente. Caso contrário, deixe este campo em branco. 0.0.0.0/0 e ::/0 não são suportados.',
'Ao usar um proxy reverso, informe seu IP ou rede. Quando vazio, nenhum proxy é confiável e o IP de origem direta é usado. 0.0.0.0/0 e ::/0 não são suportados.',
apiKeyReset: 'Redefinir chave da interface',
apiKeyResetHelper:
'O serviço associado à chave se tornará inválido. Por favor, adicione uma nova chave ao serviço',
@@ -3057,6 +3057,8 @@ const message = {
allowIPsWarning:
'Após definir a lista de IPs autorizados, somente os IPs da lista poderão acessar o serviço do painel. Você deseja continuar?',
allowIPsHelper1: 'Deixe em branco para desabilitar a restrição de IP.',
allowIPsPrivateHelper:
'Endereços de rede privada também são restritos. Para acesso local, adicione 127.0.0.1 ou ::1; para proxy reverso, configure proxies confiáveis.',
allowIPEgs: 'Um por linha. Por exemplo,\n172.16.10.111\n172.16.10.0/24',
mfa: 'Autenticação em Dois Fatores',
mfaClose: 'Desabilitar MFA reduzirá a segurança do serviço. Você deseja continuar?',

View File

@@ -2841,7 +2841,7 @@ const message = {
apiTrustedProxiesEgs:
'По одному IP-адресу или CIDR прокси-сервера в строке. Например,\n172.16.10.111\n172.16.10.0/24',
apiTrustedProxiesHelper:
'При использовании обратного прокси укажите IP-адрес или сеть прокси-сервера, чтобы корректно определить IP клиента. Если прокси не используется, оставьте поле пустым. 0.0.0.0/0 и ::/0 не поддерживаются.',
'При использовании обратного прокси укажите его IP-адрес или сеть. Если поле пустое, ни один прокси не считается доверенным и используется IP прямого источника. 0.0.0.0/0 и ::/0 не поддерживаются.',
apiKeyValidityTime: 'Срок действия ключа интерфейса',
apiKeyValidityTimeEgs: 'Срок действия ключа интерфейса (в единицах)',
apiKeyValidityTimeHelper:
@@ -3031,6 +3031,8 @@ const message = {
allowIPsWarning:
'После установки списка авторизованных IP-адресов только IP-адреса из списка смогут получить доступ к службе панели. Хотите продолжить?',
allowIPsHelper1: 'Оставьте пустым, чтобы отключить ограничение IP-адресов.',
allowIPsPrivateHelper:
'Адреса частной сети также ограничиваются. Для локального доступа добавьте 127.0.0.1 или ::1; для обратного прокси настройте доверенные прокси.',
allowIPEgs: 'По одному в строке. Например,\n172.16.10.111\n172.16.10.0/24',
mfa: '2FA авторизация',
mfaClose: 'Отключение MFA снизит безопасность службы. Хотите продолжить?',

View File

@@ -2855,7 +2855,7 @@ const message = {
apiTrustedProxiesEgs:
'Her satıra bir proxy sunucusu IPsi veya CIDR girin. Örneğin,\n172.16.10.111\n172.16.10.0/24',
apiTrustedProxiesHelper:
'Ters proxy kullanırken istemci IP adresini doğru şekilde almak için proxy sunucusunun IP adresini veya ağını girin. Kullanmıyorsanız boş bırakın. 0.0.0.0/0 ve ::/0 desteklenmez.',
'Ters proxy kullanırken IP adresini veya ağını girin. Alan boşsa hiçbir proxy güvenilir sayılmaz ve doğrudan kaynak IP kullanılır. 0.0.0.0/0 ve ::/0 desteklenmez.',
apiKeyValidityTime: 'Arayüz anahtarının geçerlilik süresi',
apiKeyValidityTimeEgs: 'Arayüz anahtarının geçerlilik süresi (dakika cinsinden)',
apiKeyValidityTimeHelper:
@@ -3064,6 +3064,8 @@ const message = {
allowIPsWarning:
'Yetkili IP adres listesini ayarladıktan sonra, yalnızca listedeki IP adresi panel servisine erişebilir. Devam etmek istiyor musunuz?',
allowIPsHelper1: 'IP adresi kısıtlamasını devre dışı bırakmak için boş bırakın.',
allowIPsPrivateHelper:
'Özel ağ adresleri de kısıtlanır. Yerel erişim için 127.0.0.1 veya ::1 ekleyin; ters proxy için güvenilir proxyleri yapılandırın.',
allowIPEgs: 'Her satıra bir tane. Örneğin,\n172.16.10.111\n172.16.10.0/24',
mfa: 'İki Aşamalı Doğrulama',
mfaClose: 'MFAyı devre dışı bırakmak servisin güvenliğini azaltabilir. Devam etmek istiyor musunuz?',

View File

@@ -2657,7 +2657,7 @@ const message = {
apiTrustedProxies: 'API 可信代理',
apiTrustedProxiesEgs: '每行填寫一個代理伺服器 IP 或 CIDR例如\n172.16.10.111\n172.16.10.0/24',
apiTrustedProxiesHelper:
'使用反向代理時,請填寫代理伺服器的 IP 或網段,以便正確取得用戶端 IP不使用時請留空。不支援 0.0.0.0/0 和 ::/0。',
'使用反向代理時,請填寫代理伺服器的 IP 或網段;留空時不信任任何代理,並使用請求的直接來源 IP。不支援 0.0.0.0/0 和 ::/0。',
apiKeyValidityTime: '介面金鑰有效期',
apiKeyValidityTimeEgs: '介面金鑰有效期(組織分)',
apiKeyValidityTimeHelper: '介面時間戳記到請求時的目前時間戳之間有效組織分設定為0時不做時間戳記校驗',
@@ -2918,6 +2918,7 @@ const message = {
allowIPsHelper: '設定授權 IP 後,僅有設定中的 IP 可以存取 1Panel 服務',
allowIPsWarning: '設定授權 IP 後,僅有設定中的 IP 可以存取 1Panel 服務,是否繼續?',
allowIPsHelper1: '授權 IP 為空時,則取消授權 IP',
allowIPsPrivateHelper: '內網地址也受限制;本機直連請加入 127.0.0.1 或 ::1反向代理請設定可信代理。',
allowIPEgs: '當存在多個授權 IP 時,需要換行顯示,例: \n172.16.10.111 \n172.16.10.0/24',
mfa: '兩步驗證',
mfaClose: '關閉兩步驗證將導致服務安全性降低,是否繼續?',

View File

@@ -2688,7 +2688,7 @@ const message = {
apiTrustedProxies: 'API 可信代理',
apiTrustedProxiesEgs: '每行填写一个代理服务器 IP 或 CIDR例如\n172.16.10.111\n172.16.10.0/24',
apiTrustedProxiesHelper:
'使用反向代理时,请填写代理服务器的 IP 或网段,以便正确获取客户端 IP不使用时请留空。不支持 0.0.0.0/0 和 ::/0。',
'使用反向代理时,请填写代理服务器的 IP 或网段;留空时不信任任何代理,并使用请求的直接来源 IP。不支持 0.0.0.0/0 和 ::/0。',
apiKeyValidityTime: '接口密钥有效期',
apiKeyValidityTimeEgs: '接口密钥有效期(单位分)',
apiKeyValidityTimeHelper: '接口时间戳到请求时的当前时间戳之间有效(单位分),设置为 0 时,不做时间戳校验',
@@ -2924,6 +2924,7 @@ const message = {
allowIPsHelper: '设置授权 IP 后,仅有设置中的 IP 可以访问 1Panel 服务',
allowIPsWarning: '设置授权 IP 后,仅有设置中的 IP 可以访问 1Panel 服务,是否继续?',
allowIPsHelper1: '授权 IP 为空时,则取消授权 IP',
allowIPsPrivateHelper: '内网地址也受限制;本机直连请添加 127.0.0.1 或 ::1反向代理请配置可信代理。',
allowIPEgs: '当存在多个授权 IP 时,需要换行显示,例: \n172.16.10.111 \n172.16.10.0/24',
mfa: '两步验证',
mfaClose: '关闭两步验证将导致服务安全性降低,是否继续?',

View File

@@ -4,6 +4,16 @@
<el-form-item :label="$t('setting.allowIPs')" prop="allowIPs">
<el-input type="textarea" :placeholder="$t('setting.allowIPEgs')" :rows="3" v-model="form.allowIPs" />
<span class="input-help">{{ $t('setting.allowIPsHelper1') }}</span>
<span class="input-help">{{ $t('setting.allowIPsPrivateHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('setting.passkeyTrustedProxies')" prop="allowIPTrustedProxies">
<el-input
type="textarea"
:placeholder="$t('setting.apiTrustedProxiesEgs')"
:rows="3"
v-model="form.allowIPTrustedProxies"
/>
<span class="input-help">{{ $t('setting.apiTrustedProxiesHelper') }}</span>
</el-form-item>
</el-form>
<template #footer>
@@ -25,9 +35,11 @@ const emit = defineEmits<{ (e: 'search'): void }>();
const form = reactive({
allowIPs: '',
allowIPTrustedProxies: '',
});
const rules = reactive({
allowIPs: [{ validator: checkIPs, trigger: 'blur' }],
allowIPTrustedProxies: [{ validator: checkIPs, trigger: 'blur' }],
});
function checkIPs(rule: any, value: any, callback: any) {
if (typeof value === 'string' && value.trim() !== '') {
@@ -36,7 +48,7 @@ function checkIPs(rule: any, value: any, callback: any) {
if (item === '') {
continue;
}
if (item.includes('0.0.0.0') || item.includes('::')) {
if (item === '0.0.0.0' || item === '0.0.0.0/0' || item === '::' || item === '::/0') {
return callback(new Error(i18n.global.t('firewall.addressFormatError')));
}
if (item.indexOf('/') !== -1) {
@@ -58,6 +70,7 @@ const formRef = ref<FormInstance>();
interface DialogProps {
allowIPs: string;
allowIPTrustedProxies: string;
}
const drawerVisible = ref();
@@ -65,6 +78,7 @@ const loading = ref();
const acceptParams = (params: DialogProps): void => {
form.allowIPs = params.allowIPs;
form.allowIPTrustedProxies = params.allowIPTrustedProxies;
drawerVisible.value = true;
};
@@ -74,7 +88,7 @@ const onSave = async (formEl: FormInstance | undefined) => {
if (!valid) return;
let title = form.allowIPs ? i18n.global.t('setting.allowIPs') : i18n.global.t('setting.unAllowIPs');
let allow = form.allowIPs
? i18n.global.t('setting.allowIPsWarning')
? `${i18n.global.t('setting.allowIPsWarning')} ${i18n.global.t('setting.allowIPsPrivateHelper')}`
: i18n.global.t('setting.unAllowIPsWarning');
ElMessageBox.confirm(allow, title, {
confirmButtonText: i18n.global.t('commons.button.confirm'),
@@ -89,7 +103,10 @@ const onSave = async (formEl: FormInstance | undefined) => {
ips.push(item);
}
}
await updateSetting({ key: 'AllowIPs', value: ips.join(',') })
await Promise.all([
updateSetting({ key: 'AllowIPs', value: ips.join(',') }),
updateSetting({ key: 'AllowIPTrustedProxies', value: form.allowIPTrustedProxies }),
])
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));

View File

@@ -210,6 +210,7 @@ const form = reactive({
expirationDays: 0,
complexityVerification: 'Disable',
allowIPs: '',
allowIPTrustedProxies: '',
bindDomain: '',
noAuthSetting: '200 - ' + i18n.global.t('setting.help200'),
noAuthSettingValue: '200',
@@ -233,6 +234,7 @@ const search = async () => {
form.expirationDays = Number(res.data.expirationDays);
form.complexityVerification = res.data.complexityVerification;
form.allowIPs = res.data.allowIPs.replaceAll(',', '\n');
form.allowIPTrustedProxies = res.data.allowIPTrustedProxies || '';
form.bindDomain = res.data.bindDomain;
form.noAuthSettingValue = res.data.noAuthSetting;
if (res.data.noAuthSetting !== '200') {
@@ -275,7 +277,10 @@ const onChangeBindDomain = () => {
domainRef.value.acceptParams({ bindDomain: form.bindDomain });
};
const onChangeAllowIPs = () => {
allowIPsRef.value.acceptParams({ allowIPs: form.allowIPs });
allowIPsRef.value.acceptParams({
allowIPs: form.allowIPs,
allowIPTrustedProxies: form.allowIPTrustedProxies,
});
};
const onChangeExpirationDays = async () => {
expirationRef.value.acceptParams({ expirationDays: form.expirationDays });