惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
Schneier on Security
The Last Watchdog
The Last Watchdog
Cyberwarzone
Cyberwarzone
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cyber Attacks, Cyber Crime and Cyber Security
L
Lohrmann on Cybersecurity
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
The Cloudflare Blog
V
V2EX
博客园_首页
博客园 - 聂微东
Vercel News
Vercel News
人人都是产品经理
人人都是产品经理
G
GRAHAM CLULEY
T
Tenable Blog
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
L
LINUX DO - 最新话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
SecWiki News
SecWiki News
博客园 - 三生石上(FineUI控件)
S
Secure Thoughts
N
News | PayPal Newsroom
T
The Blog of Author Tim Ferriss
The GitHub Blog
The GitHub Blog
T
Troy Hunt's Blog
博客园 - 【当耐特】
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
A
About on SuperTechFans
B
Blog RSS Feed
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
D
DataBreaches.Net
P
Privacy & Cybersecurity Law Blog
Schneier on Security
Schneier on Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Jina AI
Jina AI
D
Docker
P
Proofpoint News Feed

博客园 - 未来帅哥

ISAPI Rewrite 实现简单url重写、二级域名重写 SQL作业的操作全 双鱼座2010运程(终于要苦尽甘来了) wwf mvc学习 杂记 国庆节 惊喜 非常完美 最近想看连续剧或电影 无限级分类查询 端午节忙完了 - 未来帅哥 - 博客园 MOSS下面WEBPART开发小结 打开自适应子窗体,关闭子窗体刷新父窗体 SubText学习 如何扩大内需 我当圣诞老人跳舞啦! 用户中心 - 博客园 谁不是在被滚了一百次之后,才在一百零一次才叩响成功的大门. 二叉树的HTML显示
frp增加IP限制
未来帅哥 · 2025-05-23 · via 博客园 - 未来帅哥

核心设计理念

传统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 + 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 服务端修改

  1. 前端使用VUE3,增加对应的菜单和组件

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