Skip to content

一个l7vpn的设想 #18

Description

@ICKelin

先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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions