frp增加IP限制

核心设计理念

传统frp安全方案的不足

  1. 静态配置文件管理白名单IP,修改需要重启服务

  2. 分布式环境下多节点配置同步困难

  3. 缺乏实时阻断恶意IP的能力

Redis作为动态白名单存储的优势

  1. 实时生效:IP规则变更无需重启frp服务

  2. 集中管理:多台frp服务器共享同一套白名单规则

  3. 高性能验证:Redis的极速查询能力支持高频率IP检查

  4. 灵活扩展:可与安全系统集成实现动态封禁

 

技术实现解析

frp/server/proxy/proxy.go 文件中的 handleUserTCPConnection 方法中,增加了对 Redis 动态白名单的校验逻辑,确保只有授权 IP 可访问代理服务。

示例代码如下(仅展示关键片段):

func isIPAllowedV1(ctx context.Context, serverCfg *v1.ServerConfig, ip string) bool {     	xlog.FromContextSafe(ctx).Infof("Redis config: Addr=%s, Password=%s, DB=%d, EnableRedisIPWhitelist=%v",     serverCfg.RedisAddr,     serverCfg.RedisPassword,     serverCfg.RedisDB,     serverCfg.EnableRedisIPWhitelist, ) 	if !serverCfg.EnableRedisIPWhitelist { 		return true 	}  	  	rdb := redis.NewClient(&redis.Options{ 		Addr:     serverCfg.RedisAddr, 		Password: serverCfg.RedisPassword, 		DB:       serverCfg.RedisDB, 	})  	xlog.FromContextSafe(ctx).Errorf("redis check isIPAllowed db %s",serverCfg.RedisDB)  	key := serverCfg.RedisWhitelistPrefix + ip 	exists, err := rdb.Exists(ctx, key).Result() 	if err != nil { 		xlog.FromContextSafe(ctx).Errorf("redis check error for key [%s]: %v", key, err) 		return false 	} 	return exists == 1 }    // HandleUserTCPConnection is used for incoming user TCP connections. func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) { 	xl := xlog.FromContextSafe(pxy.Context()) 	defer userConn.Close()  	// 添加白名单验证 	remoteIP, _, errx := net.SplitHostPort(userConn.RemoteAddr().String()) 	if errx != nil { 		xl.Warnf("invalid remote address: %v", errx) 		return 	}  	//xl.Warnf("IP [%s] is not in whitelist, connection begin", remoteIP)  	if !isIPAllowedV1(pxy.ctx, pxy.serverCfg, remoteIP) {  	//if !isIPAllowed(pxy.ctx, &pxy.serverCfg.ServerCommon, remoteIP) { 		xl.Warnf("IP [%s] is not in whitelist, connection rejected", remoteIP) 		return 	} 	  		xl.Warnf("IP [%s] isIPAllowed  ", remoteIP) 	     // 后续代理连接逻辑 

 

WEB 服务端修改

  1. 前端使用VUE3,增加对应的菜单和组件

  2. 前端代码需要发到到目录assetsfrpsstatic
  3. 服务端增加接口,文件路径 serverdashboard_api.go

// /api/redis func (svr *Service) apiRedisWhitelist(w http.ResponseWriter, r *http.Request) { 	res := GeneralResponse{Code: 200} 	defer func() { 		log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code) 		w.WriteHeader(res.Code) 		if len(res.Msg) > 0 { 			_, _ = w.Write([]byte(res.Msg)) 		} 	}()  	log.Infof("http request: [%s]", r.URL.Path)  	// 初始化 Redis 客户端 	cfg := svr.cfg // 假设 svr.cfg 是你的 *ServerConfig 	rdb := redis.NewClient(&redis.Options{ 		Addr:     cfg.RedisAddr, 		Password: cfg.RedisPassword, 		DB:       cfg.RedisDB, 	}) 	ctx := context.Background()  	// 扫描符合前缀的所有键 	var cursor uint64 	var ipList []IPItem 	prefix := cfg.RedisWhitelistPrefix  	for { 		keys, newCursor, err := rdb.Scan(ctx, cursor, prefix+"*", 100).Result() 		if err != nil { 			res.Code = 500 			res.Msg = "redis scan error: " + err.Error() 			return 		} 		for _, key := range keys { 			// 提取 IP 			ip := strings.TrimPrefix(key, prefix)  			// 获取过期时间 			ttl, err := rdb.TTL(ctx, key).Result() 			if err != nil { 				continue 			}  			var expireAt string 			if ttl > 0 { 				expireAt = time.Now().Add(ttl).UTC().Format(time.RFC3339) 			} else if ttl == -1 { 				expireAt = "永不过期" // 永不过期 			} else { 				// 已过期或无效 				continue 			}  			ipList = append(ipList, IPItem{ 				IP:       ip, 				ExpireAt: expireAt, 			}) 		} 		if newCursor == 0 { 			break 		} 		cursor = newCursor 	}  	// 构建响应 JSON 	result := map[string]interface{}{ 		"status":    "success", 		"whitelist": ipList, 	}  	buf, _ := json.Marshal(result)  	// 构造静态响应数据 	// svrResp := map[string]interface{}{ 	// 	"status": "success", 	// 	"whitelist": []IPItem{ 	// 		{ 	// 			IP:       "192.168.1.100", 	// 			ExpireAt: "2025-06-01T12:00:00Z", 	// 		}, 	// 		{ 	// 			IP:       "10.0.0.0/24", 	// 			ExpireAt: "2025-06-10T00:00:00Z", 	// 		}, 	// 		{ 	// 			IP:       "127.0.0.1", 	// 			ExpireAt: "9999-12-31T23:59:59Z", // 永久有效 	// 		}, 	// 	}, 	// }  	//	buf, _ := json.Marshal(&svrResp) 	res.Msg = string(buf) }  // /api/addip func (svr *Service) apiRedisAddIp(w http.ResponseWriter, r *http.Request) { 	res := GeneralResponse{Code: 200} 	defer func() { 		log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code) 		w.WriteHeader(res.Code) 		if len(res.Msg) > 0 { 			_, _ = w.Write([]byte(res.Msg)) 		} 	}()  	log.Infof("http request: [%s]", r.URL.Path) 	// 解析参数 	var req struct { 		IP         string `json:"ip"` 		ExpireDays int    `json:"expire_days"` // 0 表示永不过期 	} 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 		res.Code = 400 		res.Msg = "invalid json" 		return 	} 	if strings.TrimSpace(req.IP) == "" { 		res.Code = 400 		res.Msg = "ip is empty" 		return 	}  	// Redis 	cfg := svr.cfg 	rdb := redis.NewClient(&redis.Options{ 		Addr:     cfg.RedisAddr, 		Password: cfg.RedisPassword, 		DB:       cfg.RedisDB, 	}) 	ctx := context.Background()  	key := cfg.RedisWhitelistPrefix + req.IP 	var expiration time.Duration 	if req.ExpireDays <= 0 { 		expiration = 0 // 永久 	} else { 		expiration = time.Duration(req.ExpireDays) * 24 * time.Hour 	}  	err := rdb.Set(ctx, key, "", expiration).Err() 	if err != nil { 		res.Code = 500 		res.Msg = "redis set error: " + err.Error() 		return 	}  	res.Msg = `{"status":"ok"}` }  // /api/delip func (svr *Service) apiRedisDelIp(w http.ResponseWriter, r *http.Request) { 	res := GeneralResponse{Code: 200} 	defer func() { 		log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code) 		w.WriteHeader(res.Code) 		if len(res.Msg) > 0 { 			_, _ = w.Write([]byte(res.Msg)) 		} 	}()  	var req struct { 		IP string `json:"ip"` 	} 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.IP) == "" { 		res.Code = 400 		res.Msg = "invalid request" 		return 	}  	cfg := svr.cfg 	rdb := redis.NewClient(&redis.Options{ 		Addr:     cfg.RedisAddr, 		Password: cfg.RedisPassword, 		DB:       cfg.RedisDB, 	}) 	ctx := context.Background()  	key := cfg.RedisWhitelistPrefix + req.IP 	if err := rdb.Del(ctx, key).Err(); err != nil { 		res.Code = 500 		res.Msg = "delete redis key failed: " + err.Error() 		return 	}  	res.Msg = `{"status":"deleted"}` } 

  

 

结语

frp-redis 项目通过结合 frp 的安全特性和 Redis 的灵活性,提供了一种相对安全的远程访问方案。开源这个项目是希望帮助更多开发者避免我遇到的这些问题,同时也欢迎社区贡献更好的安全实践。

在网络安全形势日益严峻的今天,作为开发者我们必须时刻保持警惕,采取纵深防御策略保护我们的服务和数据。frp-redis 只是这个过程中的一个小小尝试,但安全无小事,每一个环节都值得认真对待。

项目地址:https://github.com/wx37668827/frp-redis

发表评论

评论已关闭。

相关文章