核心设计理念
传统frp安全方案的不足
-
静态配置文件管理白名单IP,修改需要重启服务
-
分布式环境下多节点配置同步困难
-
缺乏实时阻断恶意IP的能力
Redis作为动态白名单存储的优势
-
实时生效:IP规则变更无需重启frp服务
-
集中管理:多台frp服务器共享同一套白名单规则
-
高性能验证:Redis的极速查询能力支持高频率IP检查
-
灵活扩展:可与安全系统集成实现动态封禁
技术实现解析
在 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 服务端修改
-
前端使用VUE3,增加对应的菜单和组件
- 前端代码需要发到到目录assets\frps\static
-
服务端增加接口,文件路径 server\dashboard_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 只是这个过程中的一个小小尝试,但安全无小事,每一个环节都值得认真对待。