核心设计理念
传统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 只是这个过程中的一个小小尝试,但安全无小事,每一个环节都值得认真对待。
项目地址:https://github.com/wx37668827/frp-redis
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |