国语诗 发表于 5 天前

frp增加IP限制

核心设计理念

传统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 := mapinterface{}{
                "status":    "success",
                "whitelist": ipList,
        }

        buf, _ := json.Marshal(result)

        // 构造静态响应数据
        // svrResp := mapinterface{}{
        //         "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

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: frp增加IP限制