diff --git a/.gitignore b/.gitignore index c5bd298..254ace2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ -.history -.vscode -anyproxy -anyproxy.netgo -tunneld -tunneld.netgo -tunnel/tunneld -tunnel/tunneld.netgo -logs/ +.history +.vscode +anyproxy +anyproxy-alpine +anyproxy-darwin +anyproxy-windows.exe +tunneld +tunneld-alpine +tunnel/tunneld +tunnel/tunneld-alpine +logs/ tunnel/logs/ \ No newline at end of file diff --git a/README.md b/README.md index 66c88ab..53a9fb3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # Any Proxy -anyproxy 是一个部署在Linux系统上的tcp流转发器,可以直接将本地或网络收到的请求发出,也可以将tcp流转到tunneld或SOCKS代理, 达到让不支持设置代理的网络程序通过HTTP或SOCKS代理运行的目的。可以代替Proxifier做Linux下的客户端, 也可以配合Proxifier当它的服务端。 +anyproxy 是一个部署在Linux系统上的tcp流转发器,可以直接将本地或网络收到的请求发出,也可以将请求转到tunneld或SOCKS或charles等代理。可以代替Proxifier做Linux下的客户端, 也可以配合Proxifier当它的服务端。经过跨平台编译,如果只做网络包的转发可以在windows等平台使用。 -[下载二进制包](http://cloudme.io/anyproxy) +[下载Linux包](http://cloudme.io/anyproxy) 、 [下载Mac包](http://cloudme.io/anyproxy-darwin) 、 +[下载Windows包](http://cloudme.io/anyproxy-windows.exe) 、 [下载alpine包](http://cloudme.io/anyproxy-alpine) + +提醒:请使用浏览器右键的“链接另存为”下载文件 tunneld 是一个anyproxy的服务端,部署在服务器上接收anyproxy的请求,并代理发出请求或是转到下一个tunneld。用于跨内网访问资源使用 @@ -86,7 +89,7 @@ sudo -u anyproxy ./anyproxy ./anyproxy -h ``` -注:因为用到了Linux系统函数,并不能跨平台编译。对于windows系统用户可以选择在虚拟机中启动或是win10的WSL中启动 +注:因为本地iptables转发是Linux功能,所以windows系统使用时精简掉了此部分功能 > 平滑重启 @@ -168,6 +171,7 @@ sudo iptables -t nat -D OUTPUT 2 * HTTP/1.1 keep-alive后端也能复用tcp * ~~修复iptables转发后百度贴吧无法访问的问题~~ * 转发的mysql的连接请求会一直卡住 +* ~~支持windows平台使用~~ # 感谢 diff --git a/anyproxy.go b/anyproxy.go index 25f937b..b9524e9 100644 --- a/anyproxy.go +++ b/anyproxy.go @@ -29,7 +29,7 @@ var ( func init() { flag.Usage = help.Usage - flag.StringVar(&gListenAddrPort, "l", ":3000", "Address and port to listen on") + flag.StringVar(&gListenAddrPort, "l", "", "Address and port to listen on") flag.StringVar(&gProxyServerSpec, "p", "", "Proxy servers to use") flag.IntVar(&gDebug, "debug", 0, "debug mode (0, 1, 2)") flag.StringVar(&gPprof, "pprof", "", "pprof port, disable if empty") @@ -61,6 +61,7 @@ func main() { // 是否后台运行 daemon.Daemonize(envRunMode, fd) + gListenAddrPort = config.IfEmptyThen(gListenAddrPort, conf.RouterConfig.Listen, ":3000") // 支持只输入端口的形式 if !strings.Contains(gListenAddrPort, ":") { gListenAddrPort = ":" + gListenAddrPort @@ -76,6 +77,7 @@ func main() { logging.SetDefaultLogger(logDir, cmdName, true, 3, writer) // 设置代理 + gProxyServerSpec = config.IfEmptyThen(gProxyServerSpec, conf.RouterConfig.Proxy, "") config.SetProxyServer(gProxyServerSpec) // 调试模式 diff --git a/conf/router.yaml b/conf/router.yaml index 683b50d..754390a 100644 --- a/conf/router.yaml +++ b/conf/router.yaml @@ -1,5 +1,5 @@ log: - dir: /tmp/ + dir: ./logs/ # 使用的DNS服务器 local 当前环境, remote远程, 仅当target使用remote有效 dns: local # 默认环境,local 当前环境, remote 远程, deny 禁止 @@ -8,6 +8,10 @@ target: auto tcpTarget: remote # 默认域名比对方案 match: equal +# 全局代理服务器, 优先级低于启动传参 +proxy: +# 监听端口IP, 优先级低于启动传参 +listen: # 域名 hosts: - name: github diff --git a/config/config.go b/config/config.go index f2c16a4..da29820 100644 --- a/config/config.go +++ b/config/config.go @@ -77,3 +77,14 @@ func SetListenPort(gListenAddrPort string) { } ListenPort = uint16(intNum) } + +// IfEmptyThen 取值 +func IfEmptyThen(str string, str2 string, str3 string) string { + if str == "" { + if str2 == "" { + return str3 + } + return str2 + } + return str +} diff --git a/proto/stream.go b/proto/stream.go index 4ee7163..915b15c 100644 --- a/proto/stream.go +++ b/proto/stream.go @@ -5,74 +5,13 @@ import ( "fmt" "log" "net" - "strings" - "syscall" - "github.com/keminar/anyproxy/config" "github.com/keminar/anyproxy/proto/tcp" "github.com/keminar/anyproxy/utils/trace" ) const SO_ORIGINAL_DST = 80 -// GetOriginalDstAddr 目标 -func GetOriginalDstAddr(tcpConn *net.TCPConn) (dstIP string, dstPort uint16, newTCPConn *net.TCPConn, err error) { - if tcpConn == nil { - err = errors.New("ERR: tcpConn is nil") - return - } - - // test if the underlying fd is nil - if tcpConn.RemoteAddr() == nil { - err = errors.New("ERR: clientConn.fd is nil") - return - } - - srcipport := fmt.Sprintf("%v", tcpConn.RemoteAddr()) - - newTCPConn = nil - // connection => file, will make a copy - // 会使得连接变成阻塞模式,需要自己手动 close 原来的 tcp 连接 - tcpConnFile, err := tcpConn.File() - if err != nil { - err = fmt.Errorf("GETORIGINALDST|%v->?->FAILEDTOBEDETERMINED|ERR: %v", srcipport, err) - return - } - // 旧链接关闭 - tcpConn.Close() - // 文件句柄关闭 - defer tcpConnFile.Close() - - mreq, err := syscall.GetsockoptIPv6Mreq(int(tcpConnFile.Fd()), syscall.IPPROTO_IP, SO_ORIGINAL_DST) - if err != nil { - err = fmt.Errorf("GETORIGINALDST|%v->?->FAILEDTOBEDETERMINED|ERR: getsocketopt(SO_ORIGINAL_DST) failed: %v", srcipport, err) - return - } - - // 开新连接 - newConn, err := net.FileConn(tcpConnFile) - if err != nil { - err = fmt.Errorf("GETORIGINALDST|%v->?->%v|ERR: could not create a FileConn from clientConnFile=%+v: %v", srcipport, mreq, tcpConnFile, err) - return - } - if _, ok := newConn.(*net.TCPConn); ok { - newTCPConn = newConn.(*net.TCPConn) - - // only support ipv4 - dstIP = net.IPv4(mreq.Multiaddr[4], mreq.Multiaddr[5], mreq.Multiaddr[6], mreq.Multiaddr[7]).String() - dstPort = uint16(mreq.Multiaddr[2])<<8 + uint16(mreq.Multiaddr[3]) - - ipArr := strings.Split(srcipport, ":") - // 来源和目标地址是同一个ip,且目标端口和本服务是同一个端口 - if ipArr[0] == dstIP && dstPort == config.ListenPort { - err = fmt.Errorf("may be loop call: %s=>%s:%d", srcipport, dstIP, dstPort) - } - return - } - err = fmt.Errorf("GETORIGINALDST|%v|ERR: newConn is not a *net.TCPConn, instead it is: %T (%v)", srcipport, newConn, newConn) - return -} - type stream interface { validHead() bool readRequest(from string) (canProxy bool, err error) diff --git a/proto/stream_addr.go b/proto/stream_addr.go new file mode 100644 index 0000000..a5c2375 --- /dev/null +++ b/proto/stream_addr.go @@ -0,0 +1,73 @@ +// 条件编译 https://segmentfault.com/a/1190000017846997 + +// +build !windows + +package proto + +import ( + "errors" + "fmt" + "net" + "strings" + "syscall" + + "github.com/keminar/anyproxy/config" +) + +// GetOriginalDstAddr 目标 +func GetOriginalDstAddr(tcpConn *net.TCPConn) (dstIP string, dstPort uint16, newTCPConn *net.TCPConn, err error) { + if tcpConn == nil { + err = errors.New("ERR: tcpConn is nil") + return + } + + // test if the underlying fd is nil + if tcpConn.RemoteAddr() == nil { + err = errors.New("ERR: clientConn.fd is nil") + return + } + + srcipport := fmt.Sprintf("%v", tcpConn.RemoteAddr()) + + newTCPConn = nil + // connection => file, will make a copy + // 会使得连接变成阻塞模式,需要自己手动 close 原来的 tcp 连接 + tcpConnFile, err := tcpConn.File() + if err != nil { + err = fmt.Errorf("GETORIGINALDST|%v->?->FAILEDTOBEDETERMINED|ERR: %v", srcipport, err) + return + } + // 旧链接关闭 + tcpConn.Close() + // 文件句柄关闭 + defer tcpConnFile.Close() + + mreq, err := syscall.GetsockoptIPv6Mreq(int(tcpConnFile.Fd()), syscall.IPPROTO_IP, SO_ORIGINAL_DST) + if err != nil { + err = fmt.Errorf("GETORIGINALDST|%v->?->FAILEDTOBEDETERMINED|ERR: getsocketopt(SO_ORIGINAL_DST) failed: %v", srcipport, err) + return + } + + // 开新连接 + newConn, err := net.FileConn(tcpConnFile) + if err != nil { + err = fmt.Errorf("GETORIGINALDST|%v->?->%v|ERR: could not create a FileConn from clientConnFile=%+v: %v", srcipport, mreq, tcpConnFile, err) + return + } + if _, ok := newConn.(*net.TCPConn); ok { + newTCPConn = newConn.(*net.TCPConn) + + // only support ipv4 + dstIP = net.IPv4(mreq.Multiaddr[4], mreq.Multiaddr[5], mreq.Multiaddr[6], mreq.Multiaddr[7]).String() + dstPort = uint16(mreq.Multiaddr[2])<<8 + uint16(mreq.Multiaddr[3]) + + ipArr := strings.Split(srcipport, ":") + // 来源和目标地址是同一个ip,且目标端口和本服务是同一个端口 + if ipArr[0] == dstIP && dstPort == config.ListenPort { + err = fmt.Errorf("may be loop call: %s=>%s:%d", srcipport, dstIP, dstPort) + } + return + } + err = fmt.Errorf("GETORIGINALDST|%v|ERR: newConn is not a *net.TCPConn, instead it is: %T (%v)", srcipport, newConn, newConn) + return +} diff --git a/proto/stream_windows.go b/proto/stream_windows.go new file mode 100644 index 0000000..16c462b --- /dev/null +++ b/proto/stream_windows.go @@ -0,0 +1,12 @@ +package proto + +import ( + "errors" + "net" +) + +// GetOriginalDstAddr 目标 +func GetOriginalDstAddr(tcpConn *net.TCPConn) (dstIP string, dstPort uint16, newTCPConn *net.TCPConn, err error) { + err = errors.New("ERR: windows can not work") + return +} diff --git a/proto/tunnel.go b/proto/tunnel.go index c98c0f6..9325b15 100644 --- a/proto/tunnel.go +++ b/proto/tunnel.go @@ -180,7 +180,13 @@ func (s *tunnel) dail(dstIP string, dstPort uint16) (err error) { log.Printf("%s create a new connection to server %s:%d\n", trace.ID(s.req.ID), dstIP, dstPort) } connTimeout := time.Duration(5) * time.Second - conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", dstIP, dstPort), connTimeout) // 3s timeout + var conn net.Conn + if strings.Contains(dstIP, ":") { + // tcp6 支持 + conn, err = net.DialTimeout("tcp6", fmt.Sprintf("[%s]:%d", dstIP, dstPort), connTimeout) + } else { + conn, err = net.DialTimeout("tcp", fmt.Sprintf("%s:%d", dstIP, dstPort), connTimeout) + } if err != nil { return } diff --git a/scripts/build.sh b/scripts/build.sh index e3c02ef..1c1580d 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -3,9 +3,30 @@ SCRIPT=$(readlink -f $0) ROOT_DIR=$(dirname $SCRIPT)/../ cd $ROOT_DIR -# for alpine -go build -tags netgo -o anyproxy.netgo anyproxy.go -# common +# anyproxy +echo "build anyproxy" +# for linux +echo " for linux" go build -o anyproxy anyproxy.go + +# for mac +echo " for mac" +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o anyproxy-darwin anyproxy.go + +# for windows +echo " for windows" +CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o anyproxy-windows.exe anyproxy.go + +# for alpine +echo " for alpine" +go build -tags netgo -o anyproxy-alpine anyproxy.go + +# tunneld +echo "build tunneld" +echo " for linux" go build -o tunnel/tunneld tunnel/tunneld.go + +# for alpine +echo " for alpine" +go build -tags netgo -o tunnel/tunneld-alpine tunnel/tunneld.go \ No newline at end of file diff --git a/tunnel/tunneld.go b/tunnel/tunneld.go index e8e7792..03d1deb 100644 --- a/tunnel/tunneld.go +++ b/tunnel/tunneld.go @@ -25,7 +25,7 @@ var ( func init() { flag.Usage = help.Usage - flag.StringVar(&gListenAddrPort, "l", ":3001", "Address and port to listen on") + flag.StringVar(&gListenAddrPort, "l", "", "Address and port to listen on") flag.StringVar(&gProxyServerSpec, "p", "", "Proxy servers to use") flag.IntVar(&gDebug, "d", 0, "debug mode") flag.BoolVar(&gHelp, "h", false, "This usage message") @@ -56,6 +56,7 @@ func main() { // 是否后台运行 daemon.Daemonize(envRunMode, fd) + gListenAddrPort = config.IfEmptyThen(gListenAddrPort, conf.RouterConfig.Listen, ":3001") // 支持只输入端口的形式 if !strings.Contains(gListenAddrPort, ":") { gListenAddrPort = ":" + gListenAddrPort @@ -70,6 +71,7 @@ func main() { logging.SetDefaultLogger(logDir, cmdName, true, 3, writer) // 设置代理 + gProxyServerSpec = config.IfEmptyThen(gProxyServerSpec, conf.RouterConfig.Proxy, "") config.SetProxyServer(gProxyServerSpec) server := grace.NewServer(gListenAddrPort, proto.ServerHandler) diff --git a/utils/conf/router.go b/utils/conf/router.go index 502c113..31c12df 100644 --- a/utils/conf/router.go +++ b/utils/conf/router.go @@ -26,12 +26,14 @@ type Log struct { // Router 配置文件模型 type Router struct { - Log Log `yaml:"log"` + Listen string `yaml:"listen"` //监听端口 + Log Log `yaml:"log"` //日志目录 Token string `yaml:"token"` //加密值 DNS string `yaml:"dns"` //默认的DNS服务器 Target string `yaml:"target"` //http默认访问策略 TCPTarget string `yaml:"tcpTarget"` //tcp默认访问策略 Match string `yaml:"match"` //默认域名比对 + Proxy string `yaml:"proxy"` //全局代理服务器 Hosts []Host `yaml:"hosts"` //域名列表 AllowIP []string `yaml:"allowIP"` //可以访问的客户端IP } diff --git a/utils/daemon/daemon.go b/utils/daemon/daemon.go index d0baaa1..77d6db8 100644 --- a/utils/daemon/daemon.go +++ b/utils/daemon/daemon.go @@ -7,8 +7,6 @@ import ( "flag" "log" "os" - "os/exec" - "syscall" ) var daemon = flag.Bool("daemon", false, "run app as a daemon") @@ -46,36 +44,3 @@ func IsForeground(envName string) bool { } return false } - -// Fork crete a new process -func Fork(envName string, fd *os.File, args []string) (int, error) { - cmd := exec.Command(os.Args[0], args...) - val := os.Getenv(envName) - if val == "" { //若未设置则为空字符串 - //为子进程设置特殊的环境变量标识 - os.Setenv(envName, "daemon") - } - cmd.Env = os.Environ() - cmd.Stdin = nil - //为捕获执行程序的输出,非设置新进程的os.Stdout 不要理解错 - //新进程的os.Stdout.Name()值还是默认值,但输出到/dev/stdout的这边能获取到 - //这边必须设置,否则新进程内的错误可能捕获不到 - // 用 os.NewFile(uintptr(syscall.Stderr), "/dev/stderr").WriteString("test\n") 复现 - cmd.Stdout = fd - cmd.Stderr = fd - cmd.ExtraFiles = nil - cmd.SysProcAttr = &syscall.SysProcAttr{ - // Setsid is used to detach the process from the parent (normally a shell) - // - // The disowning of a child process is accomplished by executing the system call - // setpgrp() or setsid(), (both of which have the same functionality) as soon as - // the child is forked. These calls create a new process session group, make the - // child process the session leader, and set the process group ID to the process - // ID of the child. https://bsdmag.org/unix-kernel-system-calls/ - Setsid: true, - } - if err := cmd.Start(); err != nil { - return 0, err - } - return cmd.Process.Pid, nil -} diff --git a/utils/daemon/daemon_fork.go b/utils/daemon/daemon_fork.go new file mode 100644 index 0000000..655bc5c --- /dev/null +++ b/utils/daemon/daemon_fork.go @@ -0,0 +1,44 @@ +// 条件编译 https://segmentfault.com/a/1190000017846997 + +// +build !windows + +package daemon + +import ( + "os" + "os/exec" + "syscall" +) + +// Fork crete a new process +func Fork(envName string, fd *os.File, args []string) (int, error) { + cmd := exec.Command(os.Args[0], args...) + val := os.Getenv(envName) + if val == "" { //若未设置则为空字符串 + //为子进程设置特殊的环境变量标识 + os.Setenv(envName, "daemon") + } + cmd.Env = os.Environ() + cmd.Stdin = nil + //为捕获执行程序的输出,非设置新进程的os.Stdout 不要理解错 + //新进程的os.Stdout.Name()值还是默认值,但输出到/dev/stdout的这边能获取到 + //这边必须设置,否则新进程内的错误可能捕获不到 + // 用 os.NewFile(uintptr(syscall.Stderr), "/dev/stderr").WriteString("test\n") 复现 + cmd.Stdout = fd + cmd.Stderr = fd + cmd.ExtraFiles = nil + cmd.SysProcAttr = &syscall.SysProcAttr{ + // Setsid is used to detach the process from the parent (normally a shell) + // + // The disowning of a child process is accomplished by executing the system call + // setpgrp() or setsid(), (both of which have the same functionality) as soon as + // the child is forked. These calls create a new process session group, make the + // child process the session leader, and set the process group ID to the process + // ID of the child. https://bsdmag.org/unix-kernel-system-calls/ + Setsid: true, + } + if err := cmd.Start(); err != nil { + return 0, err + } + return cmd.Process.Pid, nil +} diff --git a/utils/daemon/daemon_windows.go b/utils/daemon/daemon_windows.go new file mode 100644 index 0000000..b825282 --- /dev/null +++ b/utils/daemon/daemon_windows.go @@ -0,0 +1,29 @@ +package daemon + +import ( + "os" + "os/exec" +) + +// Fork crete a new process +func Fork(envName string, fd *os.File, args []string) (int, error) { + cmd := exec.Command(os.Args[0], args...) + val := os.Getenv(envName) + if val == "" { //若未设置则为空字符串 + //为子进程设置特殊的环境变量标识 + os.Setenv(envName, "daemon") + } + cmd.Env = os.Environ() + cmd.Stdin = nil + //为捕获执行程序的输出,非设置新进程的os.Stdout 不要理解错 + //新进程的os.Stdout.Name()值还是默认值,但输出到/dev/stdout的这边能获取到 + //这边必须设置,否则新进程内的错误可能捕获不到 + // 用 os.NewFile(uintptr(syscall.Stderr), "/dev/stderr").WriteString("test\n") 复现 + cmd.Stdout = fd + cmd.Stderr = fd + cmd.ExtraFiles = nil + if err := cmd.Start(); err != nil { + return 0, err + } + return cmd.Process.Pid, nil +}