场景
有的时候,服务器并不是有那么多端口供我们访问,甚至于22,80,443这些常用的端口都会被关闭来防止有人恶意如入侵。开放到公网的端口将会非常少,当我们部署的应用一多起来,端口可能就不够用了,这个时候,可能就需要用到端口转发,当然如果你是一个经常使用Shell的人,Putty和XShell中的隧道你可能不陌生,这边我们就模拟着实现一个类似的小工具。甚至于我们可以给这个小工具再做一次加密和鉴权,当然此次没有实现。只是实现了端口转发时公网端口的复用。
源代码
package main
import (
"flag"
"fmt"
"io/ioutil"
"net"
"os"
"os/exec"
"strconv"
"strings"
"time"
)
var (
confFilePath string
signal string
help bool
background bool
fork bool
confList []conf
)
func init() {
bingOptions()
}
func main() {
processOption()
}
//读取命令行的参数
func bingOptions() {
//使用命令
flag.StringVar(&confFilePath, "c", "tcpStation.etc", "紧跟配置文件的绝对路径或者相对路径")
flag.StringVar(&signal, "s", "", "stop 停止监听;reload 重新加载配置文件")
flag.BoolVar(&help, "h", false, "展示帮助信息")
flag.BoolVar(&background, "b", false, "后台运行")
flag.BoolVar(&fork, "f", false, "你别随便乱用")
flag.Parse()
}
//解析命令行的参数
func processOption() {
if help {
showHelp()
}
if background {
startBackgroundProcess()
}
if fork {
pid := fmt.Sprintf("%d", os.Getpid())
_ = ioutil.WriteFile("./tcp.pid", []byte(pid), os.ModePerm)
loadConfToConfStruct()
server()
}
switch signal {
case "stop":
bs, _ := ioutil.ReadFile("./tcp.pid")
cmd := "kill -9 " + string(bs)
execute(cmd)
case "reload":
// 此处没有实现重新加载的逻辑;
loadConfToConfStruct()
default:
showHelp()
}
}
func startBackgroundProcess() {
file := os.Args[0]
//> /dev/null 2>&1
cmd := "nohup " + file + " -f -c " + confFilePath + " > log.log 2>&1 &"
execute(cmd)
os.Exit(0)
}
func execute(cmd string) {
_, err := exec.Command("bash", "-c", cmd).CombinedOutput()
if err != nil {
println(err.Error())
}
}
func showHelp() {
println(`
隧道建立工具
-b 后台启动本程序
-c 指定配置文件
-h 展示本帮助
-s signal 发送信号量 stop 停止监听;reload 重新加载配置文件`)
os.Exit(-1)
}
type conf struct {
//看看ProxyIp和Port是不是-1:-1
toProxy bool
//看看targetProxy是不是-1:-1
fromProxy bool
localPort string
target string
proxy string
}
func loadConfToConfStruct() {
data, err := ioutil.ReadFile(confFilePath)
if err != nil {
println(err.Error())
os.Exit(-1)
}
dataStr := string(data)
configArr := strings.Split(dataStr, "\n")
for _, line := range configArr {
line = strings.TrimSpace(line)
println(strings.Index(line, "#"))
if strings.Index(line, "#") == 0 {
continue
}
items := strings.Split(line, ",")
if len(items) != 3 {
continue
}
confList = append(confList, conf{
toProxy: items[2] != "-1:-1", // fix by mathcec 2020.8.10 逻辑反了
fromProxy: items[1] == "-1:-1",
localPort: items[0],
target: items[1],
proxy: items[2],
})
}
//confList = []conf{{
// toProxy: false,
// fromProxy: true,
// localPort: "8080",
// targetIp: "-1",
// targetPort: -1,
// proxyIp: "-1",
// proxyPort: -1,
//}, {
// toProxy: true,
// fromProxy: false,
// localPort: "9000",
// targetIp: "127.0.0.1",
// targetPort: 9001,
// proxyIp: "127.0.0.1",
// proxyPort: 8080,
//}, {
// toProxy: false,
// fromProxy: false,
// localPort: "9002",
// targetIp: "127.0.0.1",
// targetPort: 9001,
// proxyIp: "-1",
// proxyPort: -1,
//}}
}
func server() {
for _, c := range confList {
go startProxy(c)
}
for {
time.Sleep(1 * 1000000000)
}
}
func startProxy(localConf conf) {
proxyListen, err := net.Listen("tcp", ":"+localConf.localPort)
if err != nil {
println(err.Error())
return
}
defer proxyListen.Close()
for {
proxyConn, err := proxyListen.Accept()
if err != nil {
println(err.Error())
continue
}
var buffer []byte
if localConf.fromProxy {
buffer = make([]byte, 12)
} else {
buffer = make([]byte, 6)
}
n, err := proxyConn.Read(buffer)
if err != nil {
println(err.Error())
continue
}
var targetConn net.Conn;
//判断是不是需要进行代理 需要的话 在最开始设置6位作为target
if localConf.toProxy {
buffer = append(ip2byte(localConf.target), buffer...)
targetConn, err = net.Dial("tcp", localConf.proxy)
} else {
//判断是不是代理 如果是代理
var target = ""
if localConf.fromProxy {
target = byte2ip(buffer[:6])
} else {
target = localConf.target
}
targetConn, err = net.Dial("tcp", target)
}
if err != nil {
println(err.Error())
proxyConn.Close()
continue
}
//如果代理过来的,前6为需要拿掉
if localConf.fromProxy {
//因为只拿了6个,所以不能发送其余的内容
n, err = targetConn.Write(buffer[6:n])
} else {
//这里有BUG的风险,如果没有6个呢
n, err = targetConn.Write(buffer[:])
}
if err != nil {
println(err.Error())
proxyConn.Close()
targetConn.Close()
continue
}
//在前面
go proxy(proxyConn, targetConn)
go proxy(targetConn, proxyConn)
}
}
func proxy(c1, c2 net.Conn) {
defer c1.Close()
defer c2.Close()
var buffer = make([]byte, 409600)
for {
n, err := c1.Read(buffer)
if err != nil {
break
}
n, err = c2.Write(buffer[:n])
if err != nil {
break
}
}
}
func ip2byte(ipAndPort string) []byte {
brr := strings.Split(ipAndPort, ":")
arr := strings.Split(brr[0], ".")
res := make([]byte, 6)
for i, a := range arr {
u, _ := strconv.ParseUint(a, 10, 8)
res[i] = byte(u)
}
u, _ := strconv.ParseUint(brr[1], 10, 16)
res[4] = byte(u / 256)
res[5] = byte(u - u/256*256)
return res
}
func byte2ip(byte []byte) string {
return fmt.Sprintf("%d.%d.%d.%d:%d", byte[0], byte[1], byte[2], byte[3], int(byte[4])*256+int(byte[5]))
}
原理
原理其实很简单。我们将监听模式分为两种,一种是监听本地端口并转发至目标端口,一种是监听本地端口并转发至代理端口,由代理端口转发至目标端口。
第一种很简单,当获端口监听到内容时,会首先读取一点点内容,然后开启两个个协程去处理。每个协程都会将自己接收到的输出流写入到另一端的输入流中。
第二中就比较麻烦了,也是端口监听到内容,此时会将真正的目的地写到整个流的初始位置。代理端口收到后会建立和目标端口的连接,同时将流中的标记数据删除后转发到目的端口。
配置文件是这个样子的
# 本地端口,目标IP:目标端口,代理服务器:代理端口;
# 代理服务器和代理端口不是-1:-1则认为有代理服务器
# 当目标服务器和端口是-1:-1时认为从数据包中获取目标服务器
8080,-1:-1,-1:-1
9000,127.0.0.1:9001,127.0.0.1:8080
这个时候,你可以启动一个Http服务器测试一下
package main
import "net/http"
func main() {
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
writer.Write([]byte("HelloWorld"))
})
http.ListenAndServe(":9001", nil)
}
此时按照配置文件的描述,你访问本地的9000端口,就会经过8080端口转发到9001端口。此处是将两个配置写到一起了。理论上这两个配置是要分到两个程序中。代理端口8080,-1:-1,-1:-1
需要部署在服务器上。转发端口9000,127.0.0.1:9001,127.0.0.1:8080
随便部署在一个你机器和服务器网络都通的地方。
考虑到拓展,你这个时候是不是可以在配置文件中写一个盐值对时间戳加盐,然后就可以双端校验了,甚至于,你都以可仿照SSH免密登陆的形式,搞一个密钥对。当然这不属于基本功能,这边就先不实现了。
同时,还实现了Go语言在后台执行,不过这边使用的是nohup,感觉应该还会有更好的办法。不过不太想在这种小工具中引入过多的三方库。我本人对这种小工具或者小工具包调用第三方库还是不是理解的。
网友评论