远程先敲门

背景:

1. 公司用的都是windows系统;

2. 在家办公时常要用到远程桌面连接公司的服务器;

3. 节约成本,不想投入堡垒机;

4. 要求绝对安全的登入服务器。

报怨几句:

1. 想要马儿跑,又不给马儿草,我只能说:草(四声)!

2. 又要能远程,又要安全登陆,为了工资,只能说:好!

开始分析:

1. 只能用windows自带的远程桌面,因为第三方的更不安全,自己写一个又写不出来;

2. 只好用windows的防火墙加白名单功能,这样只要控制了白名单就安全了;

3. 家用宽带外网IP大概48小时就会变动一次的,不可能加一次白名单就完整;

4. 只能用变通的方法,就是在服务器上运行一个服务,当要连接的家用的IP有变化时,修改一下防火墙白名单就行了;

5. 剩下的就是将要连接时怎样在连接前把外网IP通知给服务器?敲门!

解决方法:

1. 在公司的公网上开一个http服务,用于接收敲门的服务,具体的代码如下:

main.go


import (
	"fmt"

	"github.com/Jjmgx/mgxlog"
	"github.com/gin-gonic/gin"
)

var exit = false
var Loger, _ = mgxlog.NewMgxLog("runlog/", 10*1024*1024, 100, 3, 1000) //日志记录

func main() {
	gin.SetMode(gin.ReleaseMode)
	r := gin.New()
	r.NoRoute(func(c *gin.Context) {
		c.String(404, "Page Not Found")
	})
	r.POST("/ipset", ipset)
	r.GET("/ipset", ipset)
	go r.Run(":8080")
	for {
		var cmd string
		fmt.Scanln(&cmd)
		fmt.Println("command:", cmd)
		if cmd == "exit" {
			exit = true
			break
		} else {
			fmt.Println("unknow command")
			fmt.Println("exit exit soft")
		}
	}
}

func ipset(c *gin.Context) {
	code := c.Query("code")
	if len(code) == 0 {
		code = c.PostForm("code")
	}
	dz := c.Query("dz")
	if len(dz) == 0 {
		dz = c.PostForm("dz")
	}
	if dz == "set" {
		cip := c.ClientIP()
		if cipToRedis(code, cip) {
			Loger.Info("setip ok:", code, ":", cip)
			c.String(200, "ok")
			return
		} else {
			Loger.Error("setip err:", code, ":", cip)
		}

	} else if dz == "get" {
		fromip := c.ClientIP()
		if cip, ok := getCipFromRedis(code); ok {
			c.String(200, cip)
			Loger.Info("getip ok:", code, ":", cip, " from:", fromip)
			return
		}
	}
	c.String(400, "page not found.")
}

func getCipFromRedis(code string) (string, bool) {
	re := rc.Get(ctx, "gsetIp_"+code)
	cip, err := re.Result()
	if err != nil {
		Loger.Error(cip, err)
		return cip, false
	}
	return cip, true
}

func cipToRedis(code, cip string) bool {
	re := rc.Set(ctx, "gsetIp_"+code, cip, -1)
	s, err := re.Result()
	if err != nil {
		Loger.Error(s, err)
		return false
	}
	return true
}

rediscli.go


import (
	"context"
	"fmt"
	"runtime"
	"sync"
	"time"

	"github.com/go-redis/redis/v9"
)

var ctx = context.Background()
var lock = &sync.Mutex{} //创建互锁
var redisCluster *redis.ClusterClient
var address = []string{
	"192.168.13.110:6379",
	"192.168.13.113:6379",
	"192.168.13.111:6379",
	"192.168.13.114:6379",
	"192.168.13.112:6379",
	"192.168.13.115:6379",
}
var redispass = "xxxxxxxx"
var rc = GetRc(address, redispass)

func GetRc(address []string, password string) *redis.ClusterClient {
	if redisCluster == nil {
		lock.Lock()
		defer lock.Unlock()
		if redisCluster == nil {
			redisCluster = getRedisCluster(address, password)
			if redisCluster == nil {
				fmt.Println("null 1")
			}
		}
	}
	return redisCluster
}

func getRedisCluster(address []string, password string) *redis.ClusterClient {
	redisCluster := redis.NewClusterClient(&redis.ClusterOptions{
		Addrs:    address,
		Password: password,
		ReadOnly: false,

		//每一个redis.Client的连接池容量及闲置连接数量,而不是clusterClient总体的连接池大小。
		//实际上没有总的连接池而是由各个redis.Client自行去实现和维护各自的连接池。
		PoolSize:     20 * runtime.NumCPU(), // 连接池最大socket连接数,默认为5倍CPU数, 5 * runtime.NumCPU
		MinIdleConns: 10,                    //在启动阶段创建指定数量的Idle连接,并长期维持idle状态的连接数不少于指定数量。

		//命令执行失败时的重试策略
		MaxRetries:      0,                      // 命令执行失败时,最多重试多少次,默认为0即不重试
		MinRetryBackoff: 8 * time.Millisecond,   //每次计算重试间隔时间的下限,默认8毫秒,-1表示取消间隔
		MaxRetryBackoff: 512 * time.Millisecond, //每次计算重试间隔时间的上限,默认512毫秒,-1表示取消间隔

		//超时
		DialTimeout:  5 * time.Second, //连接建立超时时间,默认5秒。
		ReadTimeout:  3 * time.Second, //读超时,默认3秒, -1表示取消读超时
		WriteTimeout: 3 * time.Second, //写超时,默认等于读超时,-1表示取消读超时
		PoolTimeout:  4 * time.Second, //当所有连接都处在繁忙状态时,客户端等待可用连接的最大等待时长,默认为读超时+1秒。

		//IdleTimeout: 5 * time.Minute, //闲置超时,默认5分钟,-1表示取消闲置超时检查
		//MaxConnAge:  0 * time.Second, //连接存活时长,从创建开始计时,超过指定时长则关闭连接,默认为0,即不关闭存活时长较长的连接
	})
	return redisCluster
}

2. 在服务器上运行一个服务,用于将敲门的IP加白,具体代码如下:

main.go


import (
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"time"

	"github.com/Jjmgx/mgxlog"
	"github.com/kardianos/service"
)

var Loger *mgxlog.MgxLog
var yip = ""

func main() {
	Loger, _ = mgxlog.NewMgxLog("runlog/", 10*1024*1024, 100, 3, 1000)
	svcConfig := &service.Config{
		Name:        "MgxEditFW",     //服务显示名称
		DisplayName: "MgxEditFW",     //服务名称
		Description: "防火墙修改3389远程ip", //服务描述
	}
	prg := &program{}
	s, _ := service.New(prg, svcConfig)

	if len(os.Args) > 1 {
		if os.Args[1] == "install" {
			err := s.Install()
			fmt.Println("服务安装完成:", err)
			return
		}
		if os.Args[1] == "remove" {
			s.Uninstall()
			fmt.Println("服务卸载成功")
			return
		}
	}
	s.Run()

}

type program struct{}

func (p *program) Start(s service.Service) error {
	go p.run()
	return nil
}

func (p *program) Stop(s service.Service) error {
	return nil
}

func (p *program) run() error {
	for {
		work()
		time.Sleep(3 * time.Second)
	}
	return nil
}

func work() {
	resp, err := http.Get("http://ip:8080/ipset?code=yourcode........&dz=get")
	if err != nil {
		Loger.Error(err)
		time.Sleep(time.Second)
		return
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		Loger.Error(err)
		time.Sleep(time.Second)
		return
	}
	ip := string(body)
	if resp.StatusCode == 200 {
		if ip != "" && ip != yip {
			yip = ip
			cmd := exec.Command(`cmd.exe`, `/c`, `netsh advfirewall firewall set rule name ="3389" new remoteip=`+ip)
			stdout, err := cmd.StdoutPipe()
			if err != nil { //获取输出对象,可以从该对象中读取输出结果
				Loger.Error(err)
				return
			}
			defer stdout.Close()                // 保证关闭输出流
			if err := cmd.Start(); err != nil { // 运行命令
				Loger.Error(err)
			}
			if opBytes, err := ioutil.ReadAll(stdout); err != nil { // 读取输出结果
				Loger.Error(err)
			} else {
				Loger.Info(string(opBytes))
			}
			Loger.Info(ip, " changed")
		}
	}
}

3. 使用时,只要先在浏览器或者或者用curl之类软件,做一次敲门动作,敲门时只要带上自己的唯一标识,服务器自动判断到IP是否发生了改变,如果有改变会自动修改防火墙,然后直接使用远程桌面工具就可以连接到服务器了。

总结一下:

1. golang做这种小应用有点大材小用;

2. 这里使用了redis集群,也是为了学习用,本来只要存文本就行了;

3. Golang是可以开发windows服务程序的,还相当稳定!

4. 记得远程先敲门哟!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值