Open ICKelin opened 5 years ago
先mark一下。
一直有个想法,想把notr这个软件再打磨得更好一点,当前一个反馈得比较多的问题是windows版本需要安装tap驱动,而且各个平台都需要管理员权限,如果这两个问题不解决,别说用户觉得不爽了,我本身就像是束缚住了手脚,万一哪天想不开想给她加上界面,也不是那么容易。
当前要解决下面两个问题
很多vpn方案,像OpenVPN,都会用到虚拟网卡,做的是二层和三层转发,要去掉虚拟网卡,必须需要去掉二层和三层转发,要支持组网功能,以三层组网为例,就必须需要要有IP地址的概念。所以就设想了下:
可以给每个客户端编址,server维护编址表,针对网络内部而言,只是一个逻辑地址,标识客户端而已,但是这一过程对用户是透明的,任何客户端,或者在server上的程序,都能够访问得了这一逻辑地址,通过server的作为入口访问客户端,这是内网穿透,通过客户端A经过服务器访问客户端B,这是组网。这里面有一个细节需要考究,就是怎么让另一客户端或者server的数据走到目的客户端,这里说的是数据,也就是应用层数据,不包含IP头和TCP/UDP/ICMP头的。另外一个项目inject_conntrack或许能够派上用场
用一个图可以表示如下:
就notr这个软件开发当前开发而言也是存在一些平台问题,windows用tap网卡,linux/mac OS用的是tun网卡。代码本身也多了很多平台判断的逻辑,所以现在回想起来当初这么快下手去开发,也不知道是好事还是坏事,都有吧。
花了点时间验证下,发现这个思路做内网穿透是没有问题的,贴点代码,仅仅是验证,为了简化,部分是硬编码进去的:
客户端:
客户端硬编码了代理到127.0.0:8000这个地址,可以从server传递过来的。
package main import ( "flag" "fmt" "io" "net" "sync" "github.com/xtaci/smux" ) func main() { flgServer := flag.String("s", "", "server address") flag.Parse() conn, err := net.Dial("tcp", *flgServer) if err != nil { fmt.Println(err) return } sess, err := smux.Server(conn, nil) if err != nil { fmt.Println(err) return } for { stream, err := sess.AcceptStream() if err != nil { fmt.Println(err) return } go onStream(stream) } } func onStream(stream net.Conn) { remote, err := net.Dial("tcp", "127.0.0.1:8000") if err != nil { fmt.Println(err) return } wg := &sync.WaitGroup{} wg.Add(2) go func() { defer wg.Done() _, err := io.Copy(remote, stream) if err != nil { if err != io.EOF { fmt.Println(err) } return } }() go func() { defer wg.Done() _, err := io.Copy(stream, remote) if err != nil { if err != io.EOF { fmt.Println(err) } return } }() wg.Wait() }
服务器:
服务器主要两个部分,一部分是给其他程序接入用的,暂且叫access,另一部分是给客户端用的,暂且称之为server。
access.go需要依赖inject_conntrack
package main import ( "fmt" "io" "log" "net" "sync" "time" "github.com/smartwalle/going/logs" ) type Access struct { tcpListen string srv *Server } func NewAccess(tcp string, srv *Server) *Access { return &Access{ tcpListen: tcp, srv: srv, } } func (s *Access) Run() { s.tcp() } func (s *Access) tcp() error { conn, err := net.Listen("tcp", s.tcpListen) if err != nil { return err } for { client, err := conn.Accept() if err != nil { return err } go s.onTCP(client) } } func (s *Access) onTCP(conn net.Conn) { remoteIP, _, _, err := s.getRemote(conn) if err != nil { if err != io.EOF { log.Println(err) } return } stream, err := s.srv.GetStream(remoteIP) if err != nil { logs.Println(err) return } wg := &sync.WaitGroup{} wg.Add(2) go func() { defer wg.Done() io.Copy(stream, conn) }() go func() { defer wg.Done() io.Copy(conn, stream) }() wg.Wait() } func (s *Access) getRemote(conn net.Conn) (string, int, int, error) { header := make([]byte, 8) timeoutAt := time.Now().Add(time.Second * 5) conn.SetReadDeadline(timeoutAt) nr, err := io.ReadFull(conn, header) conn.SetReadDeadline(time.Time{}) if err != nil { return "", 0, 0, err } if nr != 8 { return "", 0, 0, fmt.Errorf("header length no match, expected 8 got %d", nr) } fmt.Println(header) originDst := fmt.Sprintf("%d.%d.%d.%d", header[0], header[1], header[2], header[3]) originDstPort := int(header[4]) + int(header[5])<<8 payloadlength := int(header[6]) + int(header[7])<<8 return originDst, originDstPort, payloadlength, nil }
server.go
package main import ( "errors" "fmt" "log" "net" "sync" "github.com/xtaci/smux" ) var ( errNoRoute = errors.New("no route to host") ) type ServerConfig struct { ListenAddr string Token string } type Server struct { sync.Mutex listenAddr string token string route map[string]*smux.Session dhcp *DHCP } func NewServer(c *ServerConfig) (*Server, error) { s := &Server{ listenAddr: c.ListenAddr, token: c.Token, route: make(map[string]*smux.Session), } dhcp, err := NewDHCP(&DHCPConfig{ gateway: "100.64.240.1", mask: "255.255.255.0", }) if err != nil { return nil, err } s.dhcp = dhcp return s, nil } func (s *Server) Run() error { conn, err := net.Listen("tcp", s.listenAddr) if err != nil { return err } for { client, err := conn.Accept() if err != nil { return err } go s.onConn(client) } } func (s *Server) onConn(conn net.Conn) error { s.Lock() defer s.Unlock() ip, err := s.dhcp.SelectIP("") if err != nil { return err } log.Printf("use %s for %s\n", ip, conn.RemoteAddr().String()) sess, err := smux.Client(conn, nil) if err != nil { return err } s.route[ip] = sess return nil } func (s *Server) GetStream(peer string) (net.Conn, error) { s.Lock() defer s.Unlock() sess, ok := s.route[peer] if !ok { return nil, fmt.Errorf("%s %v", peer, errNoRoute.Error()) } return sess.OpenStream() }
其实我是很烦贴代码,不贴代码不容易理解,贴代码了,就当前大环境,能定下来读代码的,好像也不是那么多。
这个验证程序运行截图:
目前利用这个思路实现的内网穿透已经发布在这里,有网友问到该如何组网,其实组网也不难,但是组网是依赖这个内核模块的,所以目前组网只能用在linux下。组网只需要将原本server的inject_conntrack模块安装在access上,将两条iptables命令在access上执行即可。
当access1希望与access2的通过内网ip建立通信时,access1上的inject_conntrack模块在数据包开始部分将需要访问的access2的地址和端口加进去,并且经过iptables的nat.output做dnat到server上监听的端口
server部分先将access1的inject_conntrack插入的目的ip读取出来,然后找到这个ip对应的上层socket,往这个socket发送数据
整个思路的核心就只有inject_conntrack的几十行代码,这个模块能够让经过的所有的点都知道数据包在nat之前的地址。
ALL ICKelin.
先mark一下。
一直有个想法,想把notr这个软件再打磨得更好一点,当前一个反馈得比较多的问题是windows版本需要安装tap驱动,而且各个平台都需要管理员权限,如果这两个问题不解决,别说用户觉得不爽了,我本身就像是束缚住了手脚,万一哪天想不开想给她加上界面,也不是那么容易。
当前要解决下面两个问题
很多vpn方案,像OpenVPN,都会用到虚拟网卡,做的是二层和三层转发,要去掉虚拟网卡,必须需要去掉二层和三层转发,要支持组网功能,以三层组网为例,就必须需要要有IP地址的概念。所以就设想了下:
可以给每个客户端编址,server维护编址表,针对网络内部而言,只是一个逻辑地址,标识客户端而已,但是这一过程对用户是透明的,任何客户端,或者在server上的程序,都能够访问得了这一逻辑地址,通过server的作为入口访问客户端,这是内网穿透,通过客户端A经过服务器访问客户端B,这是组网。这里面有一个细节需要考究,就是怎么让另一客户端或者server的数据走到目的客户端,这里说的是数据,也就是应用层数据,不包含IP头和TCP/UDP/ICMP头的。另外一个项目inject_conntrack或许能够派上用场
用一个图可以表示如下:
就notr这个软件开发当前开发而言也是存在一些平台问题,windows用tap网卡,linux/mac OS用的是tun网卡。代码本身也多了很多平台判断的逻辑,所以现在回想起来当初这么快下手去开发,也不知道是好事还是坏事,都有吧。
花了点时间验证下,发现这个思路做内网穿透是没有问题的,贴点代码,仅仅是验证,为了简化,部分是硬编码进去的:
客户端:
客户端硬编码了代理到127.0.0:8000这个地址,可以从server传递过来的。
服务器:
服务器主要两个部分,一部分是给其他程序接入用的,暂且叫access,另一部分是给客户端用的,暂且称之为server。
access.go需要依赖inject_conntrack
server.go
其实我是很烦贴代码,不贴代码不容易理解,贴代码了,就当前大环境,能定下来读代码的,好像也不是那么多。
这个验证程序运行截图:
目前利用这个思路实现的内网穿透已经发布在这里,有网友问到该如何组网,其实组网也不难,但是组网是依赖这个内核模块的,所以目前组网只能用在linux下。组网只需要将原本server的inject_conntrack模块安装在access上,将两条iptables命令在access上执行即可。
当access1希望与access2的通过内网ip建立通信时,access1上的inject_conntrack模块在数据包开始部分将需要访问的access2的地址和端口加进去,并且经过iptables的nat.output做dnat到server上监听的端口
server部分先将access1的inject_conntrack插入的目的ip读取出来,然后找到这个ip对应的上层socket,往这个socket发送数据
整个思路的核心就只有inject_conntrack的几十行代码,这个模块能够让经过的所有的点都知道数据包在nat之前的地址。
ALL ICKelin.