找回密码
 立即注册
首页 业界区 科技 frp增加IP限制

frp增加IP限制

国语诗 4 天前
核心设计理念

传统frp安全方案的不足


  • 静态配置文件管理白名单IP,修改需要重启服务
  • 分布式环境下多节点配置同步困难
  • 缺乏实时阻断恶意IP的能力
Redis作为动态白名单存储的优势


  • 实时生效:IP规则变更无需重启frp服务
  • 集中管理:多台frp服务器共享同一套白名单规则
  • 高性能验证:Redis的极速查询能力支持高频率IP检查
  • 灵活扩展:可与安全系统集成实现动态封禁
 
技术实现解析

在 frp/server/proxy/proxy.go 文件中的 handleUserTCPConnection 方法中,增加了对 Redis 动态白名单的校验逻辑,确保只有授权 IP 可访问代理服务。
示例代码如下(仅展示关键片段):
  1. func isIPAllowedV1(ctx context.Context, serverCfg *v1.ServerConfig, ip string) bool {
  2.         xlog.FromContextSafe(ctx).Infof("Redis config: Addr=%s, Password=%s, DB=%d, EnableRedisIPWhitelist=%v",
  3.     serverCfg.RedisAddr,
  4.     serverCfg.RedisPassword,
  5.     serverCfg.RedisDB,
  6.     serverCfg.EnableRedisIPWhitelist,
  7. )
  8.         if !serverCfg.EnableRedisIPWhitelist {
  9.                 return true
  10.         }
  11.        
  12.         rdb := redis.NewClient(&redis.Options{
  13.                 Addr:     serverCfg.RedisAddr,
  14.                 Password: serverCfg.RedisPassword,
  15.                 DB:       serverCfg.RedisDB,
  16.         })
  17.         xlog.FromContextSafe(ctx).Errorf("redis check isIPAllowed db %s",serverCfg.RedisDB)
  18.         key := serverCfg.RedisWhitelistPrefix + ip
  19.         exists, err := rdb.Exists(ctx, key).Result()
  20.         if err != nil {
  21.                 xlog.FromContextSafe(ctx).Errorf("redis check error for key [%s]: %v", key, err)
  22.                 return false
  23.         }
  24.         return exists == 1
  25. }
  26. // HandleUserTCPConnection is used for incoming user TCP connections.
  27. func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
  28.         xl := xlog.FromContextSafe(pxy.Context())
  29.         defer userConn.Close()
  30.         // 添加白名单验证
  31.         remoteIP, _, errx := net.SplitHostPort(userConn.RemoteAddr().String())
  32.         if errx != nil {
  33.                 xl.Warnf("invalid remote address: %v", errx)
  34.                 return
  35.         }
  36.         //xl.Warnf("IP [%s] is not in whitelist, connection begin", remoteIP)
  37.         if !isIPAllowedV1(pxy.ctx, pxy.serverCfg, remoteIP) {
  38.         //if !isIPAllowed(pxy.ctx, &pxy.serverCfg.ServerCommon, remoteIP) {
  39.                 xl.Warnf("IP [%s] is not in whitelist, connection rejected", remoteIP)
  40.                 return
  41.         }
  42.          
  43.                 xl.Warnf("IP [%s] isIPAllowed  ", remoteIP)
  44.          
  45.   // 后续代理连接逻辑
复制代码
 
WEB 服务端修改


  • 前端使用VUE3,增加对应的菜单和组件
  • 前端代码需要发到到目录assets\frps\static
  • 服务端增加接口,文件路径 server\dashboard_api.go
  1. // /api/redis
  2. func (svr *Service) apiRedisWhitelist(w http.ResponseWriter, r *http.Request) {
  3.         res := GeneralResponse{Code: 200}
  4.         defer func() {
  5.                 log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
  6.                 w.WriteHeader(res.Code)
  7.                 if len(res.Msg) > 0 {
  8.                         _, _ = w.Write([]byte(res.Msg))
  9.                 }
  10.         }()
  11.         log.Infof("http request: [%s]", r.URL.Path)
  12.         // 初始化 Redis 客户端
  13.         cfg := svr.cfg // 假设 svr.cfg 是你的 *ServerConfig
  14.         rdb := redis.NewClient(&redis.Options{
  15.                 Addr:     cfg.RedisAddr,
  16.                 Password: cfg.RedisPassword,
  17.                 DB:       cfg.RedisDB,
  18.         })
  19.         ctx := context.Background()
  20.         // 扫描符合前缀的所有键
  21.         var cursor uint64
  22.         var ipList []IPItem
  23.         prefix := cfg.RedisWhitelistPrefix
  24.         for {
  25.                 keys, newCursor, err := rdb.Scan(ctx, cursor, prefix+"*", 100).Result()
  26.                 if err != nil {
  27.                         res.Code = 500
  28.                         res.Msg = "redis scan error: " + err.Error()
  29.                         return
  30.                 }
  31.                 for _, key := range keys {
  32.                         // 提取 IP
  33.                         ip := strings.TrimPrefix(key, prefix)
  34.                         // 获取过期时间
  35.                         ttl, err := rdb.TTL(ctx, key).Result()
  36.                         if err != nil {
  37.                                 continue
  38.                         }
  39.                         var expireAt string
  40.                         if ttl > 0 {
  41.                                 expireAt = time.Now().Add(ttl).UTC().Format(time.RFC3339)
  42.                         } else if ttl == -1 {
  43.                                 expireAt = "永不过期" // 永不过期
  44.                         } else {
  45.                                 // 已过期或无效
  46.                                 continue
  47.                         }
  48.                         ipList = append(ipList, IPItem{
  49.                                 IP:       ip,
  50.                                 ExpireAt: expireAt,
  51.                         })
  52.                 }
  53.                 if newCursor == 0 {
  54.                         break
  55.                 }
  56.                 cursor = newCursor
  57.         }
  58.         // 构建响应 JSON
  59.         result := map[string]interface{}{
  60.                 "status":    "success",
  61.                 "whitelist": ipList,
  62.         }
  63.         buf, _ := json.Marshal(result)
  64.         // 构造静态响应数据
  65.         // svrResp := map[string]interface{}{
  66.         //         "status": "success",
  67.         //         "whitelist": []IPItem{
  68.         //                 {
  69.         //                         IP:       "192.168.1.100",
  70.         //                         ExpireAt: "2025-06-01T12:00:00Z",
  71.         //                 },
  72.         //                 {
  73.         //                         IP:       "10.0.0.0/24",
  74.         //                         ExpireAt: "2025-06-10T00:00:00Z",
  75.         //                 },
  76.         //                 {
  77.         //                         IP:       "127.0.0.1",
  78.         //                         ExpireAt: "9999-12-31T23:59:59Z", // 永久有效
  79.         //                 },
  80.         //         },
  81.         // }
  82.         //        buf, _ := json.Marshal(&svrResp)
  83.         res.Msg = string(buf)
  84. }
  85. // /api/addip
  86. func (svr *Service) apiRedisAddIp(w http.ResponseWriter, r *http.Request) {
  87.         res := GeneralResponse{Code: 200}
  88.         defer func() {
  89.                 log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
  90.                 w.WriteHeader(res.Code)
  91.                 if len(res.Msg) > 0 {
  92.                         _, _ = w.Write([]byte(res.Msg))
  93.                 }
  94.         }()
  95.         log.Infof("http request: [%s]", r.URL.Path)
  96.         // 解析参数
  97.         var req struct {
  98.                 IP         string `json:"ip"`
  99.                 ExpireDays int    `json:"expire_days"` // 0 表示永不过期
  100.         }
  101.         if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  102.                 res.Code = 400
  103.                 res.Msg = "invalid json"
  104.                 return
  105.         }
  106.         if strings.TrimSpace(req.IP) == "" {
  107.                 res.Code = 400
  108.                 res.Msg = "ip is empty"
  109.                 return
  110.         }
  111.         // Redis
  112.         cfg := svr.cfg
  113.         rdb := redis.NewClient(&redis.Options{
  114.                 Addr:     cfg.RedisAddr,
  115.                 Password: cfg.RedisPassword,
  116.                 DB:       cfg.RedisDB,
  117.         })
  118.         ctx := context.Background()
  119.         key := cfg.RedisWhitelistPrefix + req.IP
  120.         var expiration time.Duration
  121.         if req.ExpireDays <= 0 {
  122.                 expiration = 0 // 永久
  123.         } else {
  124.                 expiration = time.Duration(req.ExpireDays) * 24 * time.Hour
  125.         }
  126.         err := rdb.Set(ctx, key, "", expiration).Err()
  127.         if err != nil {
  128.                 res.Code = 500
  129.                 res.Msg = "redis set error: " + err.Error()
  130.                 return
  131.         }
  132.         res.Msg = `{"status":"ok"}`
  133. }
  134. // /api/delip
  135. func (svr *Service) apiRedisDelIp(w http.ResponseWriter, r *http.Request) {
  136.         res := GeneralResponse{Code: 200}
  137.         defer func() {
  138.                 log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
  139.                 w.WriteHeader(res.Code)
  140.                 if len(res.Msg) > 0 {
  141.                         _, _ = w.Write([]byte(res.Msg))
  142.                 }
  143.         }()
  144.         var req struct {
  145.                 IP string `json:"ip"`
  146.         }
  147.         if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.IP) == "" {
  148.                 res.Code = 400
  149.                 res.Msg = "invalid request"
  150.                 return
  151.         }
  152.         cfg := svr.cfg
  153.         rdb := redis.NewClient(&redis.Options{
  154.                 Addr:     cfg.RedisAddr,
  155.                 Password: cfg.RedisPassword,
  156.                 DB:       cfg.RedisDB,
  157.         })
  158.         ctx := context.Background()
  159.         key := cfg.RedisWhitelistPrefix + req.IP
  160.         if err := rdb.Del(ctx, key).Err(); err != nil {
  161.                 res.Code = 500
  162.                 res.Msg = "delete redis key failed: " + err.Error()
  163.                 return
  164.         }
  165.         res.Msg = `{"status":"deleted"}`
  166. }
复制代码
  
 
结语

frp-redis 项目通过结合 frp 的安全特性和 Redis 的灵活性,提供了一种相对安全的远程访问方案。开源这个项目是希望帮助更多开发者避免我遇到的这些问题,同时也欢迎社区贡献更好的安全实践。
在网络安全形势日益严峻的今天,作为开发者我们必须时刻保持警惕,采取纵深防御策略保护我们的服务和数据。frp-redis 只是这个过程中的一个小小尝试,但安全无小事,每一个环节都值得认真对待。
项目地址:https://github.com/wx37668827/frp-redis

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册