背景:
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. 记得远程先敲门哟!