首页/文章列表/文章详情

frp增加IP限制

编程知识672025-05-23评论

核心设计理念

传统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 + ipexists, 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. 前端代码需要发到到目录assets\frps\static
  3. 服务端增加接口,文件路径 server\dashboard_api.go

// /api/redisfunc (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 是你的 *ServerConfigrdb := redis.NewClient(&redis.Options{Addr: cfg.RedisAddr,Password: cfg.RedisPassword,DB: cfg.RedisDB,})ctx := context.Background()// 扫描符合前缀的所有键var cursor uint64var ipList []IPItemprefix := cfg.RedisWhitelistPrefixfor {keys, newCursor, err := rdb.Scan(ctx, cursor, prefix+"*", 100).Result()if err != nil {res.Code = 500res.Msg ="redis scan error:" + err.Error()return}for _, key := range keys {// 提取 IPip := strings.TrimPrefix(key, prefix)// 获取过期时间ttl, err := rdb.TTL(ctx, key).Result()if err != nil {continue}var expireAt stringif 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}// 构建响应 JSONresult := 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/addipfunc (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 = 400res.Msg ="invalid json"return}if strings.TrimSpace(req.IP) =="" {res.Code = 400res.Msg ="ip is empty"return}// Rediscfg := svr.cfgrdb := redis.NewClient(&redis.Options{Addr: cfg.RedisAddr,Password: cfg.RedisPassword,DB: cfg.RedisDB,})ctx := context.Background()key := cfg.RedisWhitelistPrefix + req.IPvar expiration time.Durationif 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 = 500res.Msg ="redis set error:" + err.Error()return}res.Msg = `{"status":"ok"}`}// /api/delipfunc (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 = 400res.Msg ="invalid request"return}cfg := svr.cfgrdb := redis.NewClient(&redis.Options{Addr: cfg.RedisAddr,Password: cfg.RedisPassword,DB: cfg.RedisDB,})ctx := context.Background()key := cfg.RedisWhitelistPrefix + req.IPif err := rdb.Del(ctx, key).Err(); err != nil {res.Code = 500res.Msg ="delete redis key failed:" + err.Error()return}res.Msg = `{"status":"deleted"}`}

  

 

结语

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

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

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

神弓

未来帅哥

这个人很懒...

用户评论 (0)

发表评论

captcha