ICKelin / article

读书笔记,博客文章
MIT License
116 stars 18 forks source link

一个l7vpn的设想 #18

Open ICKelin opened 5 years ago

ICKelin commented 5 years ago

先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上执行即可。

整个思路的核心就只有inject_conntrack的几十行代码,这个模块能够让经过的所有的点都知道数据包在nat之前的地址。

ALL ICKelin.