相关文章:

一个l7vpn的设想

先mark一下。

一直有个想法,想把notr这个软件再打磨得更好一点,当前一个反馈得比较多的问题是windows版本需要安装tap驱动,而且各个平台都需要管理员权限,如果这两个问题不解决,别说用户觉得不爽了,我本身就像是束缚住了手脚,万一哪天想不开想给她加上界面,也不是那么容易。

当前要解决下面两个问题

  • 去掉虚拟网卡
  • 支持组网,当前没有组网功能,但是在当前基础之上,想要做到组网其实是特别简单的。所以调整完之后,我也希望能够很好都适配组网功能。

很多vpn方案,像OpenVPN,都会用到虚拟网卡,做的是二层和三层转发,要去掉虚拟网卡,必须需要去掉二层和三层转发,要支持组网功能,以三层组网为例,就必须需要要有IP地址的概念。所以就设想了下:

可以给每个客户端编址,server维护编址表,针对网络内部而言,只是一个逻辑地址,标识客户端而已,但是这一过程对用户是透明的,任何客户端,或者在server上的程序,都能够访问得了这一逻辑地址,通过server的作为入口访问客户端,这是内网穿透,通过客户端A经过服务器访问客户端B,这是组网。这里面有一个细节需要考究,就是怎么让另一客户端或者server的数据走到目的客户端,这里说的是数据,也就是应用层数据,不包含IP头和TCP/UDP/ICMP头的。另外一个项目inject_conntrack或许能够派上用场

用一个图可以表示如下:

image

就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()
}

其实我是很烦贴代码,不贴代码不容易理解,贴代码了,就当前大环境,能定下来读代码的,好像也不是那么多。

这个验证程序运行截图:

image

image


目前利用这个思路实现的内网穿透已经发布在这里,有网友问到该如何组网,其实组网也不难,但是组网是依赖这个内核模块的,所以目前组网只能用在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.