GithubHelp home page GithubHelp logo

article's Introduction

相逢即是有缘 👋

我的产品

这是我们团队开发的一些商业项目,目前已在跨境电商,直播,网络组网,远程办公以及远程设备管理等场景获得了一些付费客户,有类似需求的朋友可以联系我。

  • 😄 全球加速 - gtun项目的商业化产品,实现快速,高效的网络加速服务
  • 😉 SD-WAN网络平台 - cframe项目的商业化产品,实现企业分支,分支与云,云与云之间互联的基础设施
  • 😱 零信任网关 - 一个SDP隐身网关,技术上基于内网穿透+VPN,内网穿透实现隐身,VPN实现认证
  • 😉 我要投屏 - 一个会议室投屏产品,使用浏览器就能投屏,我们在和客户打交道的过程中,一个团队成员觉得这块挺有意思,业余时间尝试做的一款产品

90%的功能都具备免费版本,由免费用户辐射到企业决策者是我们的产品设计理念。

开源项目

这是我个人开发的一些开源项目。

  • 😄 gtun - golang开发的基于tproxy+kcp技术的IP加速器,搭载软路由最香
  • 😱 cframe - 一个网格VPN项目
  • 😉 opennotr - 一款开源的内网穿透项目
  • 😱 optw - 一个封装的连接复用库,纯粹wrapper,封装smux,kcp,quic为统一接口
  • 😉 zta(zero trust access) - 一款零信任访问项目,结合系列视频(Youtube | B站)可以一步一步了解项目的实现过程,目前在开发中,感兴趣的朋友可以参与进来。

一些文章、博客、视频

这是我的个人博客地址,日常会在上面分享一些技术 - 个人博客,会涉及到以下分类的文章。

这是我的一些视频平台地址,网名是【呼噜聊网络】,不想写文章的时候就录制视频(数据感人!)

ICKelin's github stats

article's People

Contributors

ickelin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

article's Issues

iptables mangle + 策略路由进行分流

本周在网关上解决一个网卡分流的问题,简单可以描述为tcp数据走eth0,udp数据走eth1,icmp数据走eth2。研究了下,发现可以利用iptables的mangle表和策略路由来进行流量分发。

在了解iptables之前,我觉得应该首先了解下数据包在内核路由子系统前后会经过那些处理以及linux防火墙里面非常核心的netfilter系统。

image

这上面是netfilter系统非常典型的几个钩子处理,本人最初接触netfilter的时候是在学校的一次大作业当中开发的一个包过滤防火墙,职业辗转,最后还是回到来netfilter相关的技术,当然也没有这么底层了,更多的谁在用户层徘徊。

在了解netfilter之后就得开始考虑netfilter和iptables之间的关系了,netfilter可能很多开发人员接触的不多,但是或多或少了解过iptabes,其实很多人说iptables是linux下的防火墙,这个说法我觉得不太合适,iptables是linux防火墙的一部分,简单理解iptables其实只是防火墙的前端,给用户用的,还有就是给内核模块发送规则操作命令,真正实现防火墙功能和维护规则表的其实是内核模块。

iptables的四表五链。

四表:raw,mangle,nat,filter四表
五链:也就是上图当中的PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING五个hook点。

链和表的关系是1:n的关系,一个hook点上可以有多个表,此时就有一个表的优先级的问题,四表的优先级分别是
raw>mangle>nat>filter

表和链的关系是一个hook点上可以有多个表,具体对应关系如表1所示:

表1:

table hook
raw PREROUTING,OUTPUT
mangle PREROUTING, INPUT, FORWARD,OUTPUT,POSTROUTING
nat PREROUTING,POSTROUTING,OUTPUT
filter INPUT,FORWARD,OUTPUT

通常来说,filter主要用于包过滤防火墙的实现,nat表主要做DNAT和SNAT的功能,mangle表主要用来做qos,其一个特点是可以修改数据包。raw表主要用来加快数据包穿越防火墙的速度,提高防火墙的性能,目前raw表还没有接触过相关的使用场景。

这篇文章主要使用mangle表。

问题

有了iptables的一些背景之后,开始解决我们遇到的问题。

当前模式:我们在网关上有一个应用加速的程序,负责对tcp,udp,icmp三种协议的数据包进行加速,tcp和udp都用到了conntrack相关的内容,icmp由于不是在用户层实现的,所以需要用netfilter的queue机制让用户态接管icmp

面对的问题:首先一个是conntrack的问题,我们需要跟内核通信获取conntrack记录,对于tcp而言没太大问题,除非有非常非常多的短链接,因为只需要在握手完成之后就不用在查conntrack记录了,对于udp而言,每个数据包都需要与内核通信查找conntrack记录,而关于icmp的问题,用过libnetfilter_queue或者说用过netlink的都知道,这玩意并不是很好用,久不久就会buffer unavailable,当然icmp数据通常也不会造成这一问题。

所以可能会回到一些比较成熟的方案去实现udp和icmp的加速,tcp依旧保持当前状态。比较成熟的加速方案,大部分都会想到虚拟网卡。于是就有了当前的分流模式。

image

这里面通过mangle给数据包设置mark值,然后添加路由表和mark值匹配,最后在将表规则route到对应的网卡

mangle表set-mark

iptables -t mangle -A PREROUTING -p udp -j MARK --set-mark 10
iptables -t mangle -A PREROUTING -p icmp -j MARK --set-mark 11

建表

ip rule add from all fwmark 10 table 10
ip rule add from all fwmark 11 table 11

route
ip route add default dev udp_0 table 10
ip route add default dev icmp_0 table 11

这样就完成了网卡协议分流的操作,后续就是程序处理了,具体情况具体分析。我这边后续处理可以简单描述为
0)解析协议栈,把ip头和udp头解析并剥离
1)读取udp数据
2)通过加速通道发送出去
3)接收加速通道返回回来的消息
4)通过原始套接字构造udp响应返回给客户端,从而替换原来方式。

notes

**/**
 * 
 ***/**
function getSubmitData() {
    let subData = {
        "radio":wrlRadioBind,
        "action": "add",
        "ssidIndex": 0,
        "macFilterEn": cmp.getComponent("macFilterEn").getValue(),
        "filterMode": cmp.getComponent("filterMode").getValue(),
        "macList": tableCmp.getValue()
    };
    return subData;
}**

为提高英语,花两个小时写了个爬取voa上的英文新闻

为提高英语,花两个小时写了个爬取voa上的英文新闻,主要用了goquery这个库,没其他的了,关键是能够解决我自身的需求。如果您很关注性能,或者非常纠结与代码的美观,那么这段代码可能会被喷,anyway,我认为能够用最快的方式解决我问题的就ok了,毕竟我写这个的目的是解决实际的问题。

项目部署在我的树莓派上,使用Notr进行内网穿透,可以通过voa 或者voa 进行访问。


package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/PuerkitoBio/goquery"
)

var (
	defaultSite  = "http://www.51voa.com"
	defaultEntry = "http://www.51voa.com/Technology_Report_1.html"
	defaultDir   = "./news"
)

type VOA struct {
	Title    string `json:"title"`
	VoiceUrl string `json:"voice_url"`
	Content  string `json:"content"`
}

func main() {
	go spider(defaultEntry)

	server := gin.Default()
	server.GET("/", func(ctx *gin.Context) {
		dir, err := os.Stat(defaultDir)
		if err != nil {
			log.Printf("stat ./ fail: %v\n", err)
			return
		}

		if dir.IsDir() {
			files, err := ioutil.ReadDir(dir.Name())
			if err != nil {
				log.Printf("read dir fail: %v\n", err)
				return
			}

			list := ""
			for _, file := range files {
				if strings.HasSuffix(file.Name(), ".html") {
					name := strings.TrimSuffix(file.Name(), ".html")

					sp := strings.Split(name, "-")
					if len(sp) > 0 {
						// ignore last number
						if _, err := strconv.Atoi(sp[len(sp)-1]); err == nil {
							sp = sp[:len(sp)-1]
						}
						name = strings.Join(sp, " ")
						item := fmt.Sprintf("<li><a href=\"%s/%s\">%s</a>", defaultDir, file.Name(), name)
						list += item
					}
				}
			}

			indextemp := fmt.Sprintf(index, list)
			ctx.Writer.Write([]byte(indextemp))
		}
	})

	server.Static("/news", "./news")

	server.Run(":8000")
}

// download news
// entry => #list.li.a => get title and detail url
// detail => #mp3.href
// detail => #content
// store without VOA_Special_English
func spider(entry string) {
	for {
		doc, err := goquery.NewDocument(entry)
		if err != nil {
			fmt.Println("open document fail: ", err)
                         time.Sleep(time.Second * 10)
                         // fix: do not return
			continue
		}

		doc.Find("#list").Find("li").Find("a").Each(func(idx int, element *goquery.Selection) {
			href, exist := element.Attr("href")
			if !exist {
				fmt.Println("element not contains href attribute")
				return
			}

			title, err := element.Html()
			if err != nil {
				fmt.Println("element not conains html content: ", err)
				return
			}

			url := defaultSite + href
			detail, err := goquery.NewDocument(url)
			if err != nil {
				fmt.Println("got detail page fail: ", err)
				return
			}

			mp3 := detail.Find("#mp3")
			mp3url, exist := mp3.Attr("href")
			if !exist {
				fmt.Println("not contains mp3 download url")
				return
			}

			content, err := detail.Find("#content").Html()
			if err != nil {
				fmt.Println("not contains voa news content: ", err)
				return
			}

			voa := &VOA{
				Title:    title,
				VoiceUrl: mp3url,
				Content:  content,
			}

			prefix := "VOA_Special_English"
			index := strings.LastIndex(href, "VOA_Special_English") + len(prefix)

			href = href[index:]

			body := fmt.Sprintf(template, voa.Title, voa.Title, voa.VoiceUrl, voa.Content)

			fp, err := os.Create(defaultDir + "/" + href)
			if err != nil {
				fmt.Println("create file fail: ", err)
				return
			}
			defer fp.Close()
			fp.Write([]byte(body))
		})

		time.Sleep(time.Hour * 24)
	}
}

var template = `
	<html>
		<title>%s</title>	
		<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">    
		<meta name="referrer" content="never">
		<body>
			<div class="container" style="padding:50px">
				<h2>%s</h2>
				<p>
					<audio src="%s" controls="controls">
					Your browser does not support the audio element.
					</audio>
				</p>
				<p>
					%s
				<p>
			</div>
		</body>
	</html>
`

var index = `
	<html>
	<title>voa article</title>	
	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">    

	<body>
		<div class="container ">
			<h2>新闻列表</h2>
			<p>
				<ul>
					%s
				</ul>
			</p>
		</div>
	</body>
	</html>
`

一个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.

DNS原理与动态DNS

DNS是非常常用的网络基础设施软件,但是如果不是做软件开发的,很多人都接触不到,很多做应用开发的,可能接触DNS服务器的机会也不多,但是不管怎样,DNS是非常非常非常重要的网络基础软件。

这篇文章记录了对DNS的一些基本理解以及在notr内网穿透当中使用到的非常重要的动态域名解析这一功能。

DNS解决什么问题

需要明确的一点,互联网上的通信寻址是通过ip地址来寻址的,打个比方,如果从你的电脑上ping 1.1.1.1,首先判断1.1.1.1是否和你处于同一个子网,如果属于同一个子网,则按照内网通信的基本流程,先进行ARP广播,查找ip地址为1.1.1.1的mac地址,然后通过mac地址进行通信,如果不属于同一个子网,则也要先进行arp广播,不过查找的是网关ip地址的mac地址,将数据包发给网关,网关匹配路由之后,继续转发给他的上一级路由,依次迭代,直到到达1.1.1.1所在的机器。

如果纯粹使用ip地址通信,通信是没问题的,但是互联网发展太快了,五六年前的规模和现在完全一个层面,五六年前,我还在学jsp,学MFC,现在MFC也叫没饭吃,设计互联网的人很早就想到了这点,ip地址太难记了,要记住所有网站的ip地址显然是不可能的,所以他们设计了域名系统,在ip地址与域名之间构建一个映射关系,通过域名关联到ip地址,然后用户只需要记住域名即可,记住www.notr.tech总比记住120.25.214.63要容易得多。但是这又会产生一个问题,用户每次访问www.notr.tech的时候,都需要先通过互联网获取到www.notr.tech的ip地址,每个域名相对直接使用ip而言,都多了一步域名解析的步骤。于是为了加速域名解析的过程,DNS服务器都会设置有高速缓存,不仅如此,每台电脑针对域名查询,也做了高速缓存,不仅如此,有的浏览器内部也做了域名高速缓存。所以就有了如果某浏览器是带有dns缓存功能的,访问一个网站时,首先从自身的缓存看有没有记录,如果有,直接用,如果没有,看系统DNS缓存是否有记录,如果有,直接用,如果没有,发起DNS查询,如果刚好你的网关配备有DNS服务器,而且dhcp服务器给你电脑下发的DNS就是你的网关地址,那么你的网关先从自身缓存查,依次迭代下去,所以,DNS缓存提高了域名解析的效率。

DNS基本流程

DNS采用UDP协议进行交互,交互可以归纳为下面的流程

image

这里将DHCP也纳入其中,DHCP跟DNS的关系仅仅是:PC启动的时候会发送DHCP请求广播,DHCP服务器收到这一广播之后会响应,会告诉PC,你可以使用a.b.c.d这个ip,网关地址是a.b.c.1,DNS地址为1.1.1.1之类的信息。这个很重要,可以通过DHCP控制所有用户电脑的网关和DNS,也就是说可以将下发的网关和DNS指向它自身,然后所有流量和域名查询都经过它。

dns服务器本地无缓存时,会不断迭代同一个过程,以yingjiu.notr.tech为例先找到根服务器,询问是否有yingjiu.notr.tech的ip地址,根服务器响应没有,但是你可以到ns1.china.com去查询,ns1.china.com的ip地址是xxxxx。于是本地dns服务器向ns1.china.com进行域名解析,ns1.china.com发现本地也没有相关的A记录,但是有个ns记录,记录值为ns.notr.tech,于是ns1.china.com响应给本地dns服务器,告诉它,我没有相关记录,但是我知道谁可以解决你这个问题,你可以到ns.notr.tech进行解析,ns.notr.tech查本地记录,发现有对应的记录值,于是响应该记录值,本地DNS服务器拿到域名解析的结果之后,响应给pc,最终完成域名解析。

DNS服务器实现有许多细节,但是如果业务需求不复杂,可以考虑从以下两个角度进行简化

  • 只实现A,AAAA,以及NS记录
  • 只实现dns代理,域名解析交给上游,只需要知道,上游返回的结果肯定是可以返回给pc的。

动态DNS

以下内容摘自百度百科:

动态域名解析服务,是将用户的动态IP地址映射到一个固定的域名解析服务上,用户每次连接网络的时候,客户端程序就会通过信息传递把该主机的动态IP地址传送给位于服务商主机上的服务器程序,服务程序负责提供DNS服务并实现动态域名解析。就是说DDNS捕获用户每次变化的IP地址,然后将其与域名相对应,这样域名就可以始终解析到非固定IP的服务器上,互联网用户通过本地的域名服务器获得网站域名的IP地址,从而可以访问网站的服务。

简单理解动态DNS即为:当DNS具备能够动态添加,删除,修改某个域名记录之时,就具备动态DNS的基本能力。

打个比方:你在使用某个动态DNS服务,你配置家庭出口路由器映射的ip地址映射到router.home.me,但是家庭路由器每次启动时ip地址可能会发生修改,原因是运营商之前下发的ip地址可能被分配给其他用户,所以这时候如果DNS不具备修改的能力,那么访问router.home.me解析到的ip地址即为其他客户的ip地址,这时候就需要DNS能够提供一个接口让家庭路由器感知到ip地址发生了改变,将router.home.me的值进行相对应的调整。也就是做router.home.me这个域名的修改操作。

所以在无固定ip地址的情况下,采用动态DNS具备一定的用处,这里介绍我动态DNS的使用场景:

Notr是一款内网穿透服务,其中动态DNS占了非常大的作用,客户端每次分配的域名是固定的,但是域名映射关系每次都是不固定的,因为我内部需要根据当前各个节点的使用情况与负载进行动态调整,选出一个节点为该客户端提供内网映射的接入口。具体可以参考Notr时序图NotrController与NotrNs交互

image

基于CoreDNS和etcd实现动态域名解析

在我开发的项目notr内网穿透当中引入DNS来解决动态配置*.notr.tech的A记录的问题,针对用户test,每次客户端连上来之后都要设置test.notr.tech的A记录为当前连接的服务器。目前DNS版本还非常简单,已经开源成notrns项目。但是这个项目还有几个问题:

  • 数据存储在boltdb当中,只能单点用
  • 如果流量比较大,需要部署多个dns节点,数据同步问题比较麻烦
  • 性能有待测试,能工作,但是工作的极限还不知道

通过github了解到CoreDNSetcd两个项目,就琢磨着用CoreDNS代替notrns来做动态域名解析,使用etcd来做存储。

按照这种流程进行,迁移方便,开发量也少,扩展起来也容易。

测试

  1. 启动etcd
  2. 启动coredns

CoreFile:


notr.tech {
    etcd  {
        path /skydns
        endpoint http://localhost:2379
        upstream
    }
    log
}

  1. 使用etctrl设置域名解析
➜  bin git:(master) ./etcdctl put /skydns/tech/notr/yingjiu/ '{"host":"192.168.1.2"}'

OK
➜  bin git:(master) nslookup yingjiu.notr.tech 127.0.0.1
Server:		127.0.0.1
Address:	127.0.0.1#53

Name:	yingjiu.notr.tech
Address: 192.168.1.2

➜  bin git:(master) ./etcdctl put /skydns/tech/notr/yingjiu/ '{"host":"192.168.1.3"}'

OK
➜  bin git:(master) nslookup yingjiu.notr.tech 127.0.0.1
Server:		127.0.0.1
Address:	127.0.0.1#53

Name:	yingjiu.notr.tech
Address: 192.168.1.3

➜  bin git:(master)

接下来只需要在registry将etcd client集成进去即可,改造完之后整个软件变成了下图所示的流程。

notr架构

  1. 每次启动一个服务端节点之后,会往registry发送当前节点的信息,目的是让registry做负载均衡以及根据地理位置进行调度,同时服务端节点也可以随时插拔,随时都可以添加和删除节点。
  2. 把节点信息写入数据库,当前使用mongodb
  3. 步骤一和步骤二初始化完成之后,用户使用客户端,先和registry节点连接,获取接入的服务节点的信息
  4. registry从数据库中取出在步骤二中存储的节点信息,并根据节点当前连接的客户端数量和地理位置进行一轮选择,首选地理位置最近的,目前只划两个区,**区和海外区,然后再根据客户端连接数量排序选择连接数最少的。
  5. 客户端拿到节点信息之后和server建立tcp长连接
  6. server节点需要从registry请求用户,限速等信息
  7. 从数据库取出数据
  8. 生成域名解析记录,将当前用户等域名映射到它当前连接到server的公网IP
  9. 每次需要域名解析时,由于配置了ns记录,用户的域名解析请求最终会到coredns,coredns再从etcd中取出

一个c实现的channel

最近看到一段用c实现的channel,感觉挺小巧精妙的,就好好研究了下。

包含两个基本操作:1、往channel发送数据;2、从channel中读取数据

本质上是用一段内存来实现一个环形队列。用两个游标来指向队头和队尾。每次要发送时,往队尾加数据,要读取时,从队头游标获取数据,当内存块不足时进行扩容,扩容机制采用的是原来内存块*2的方式。针对并发操作,用锁来保证同一时刻环形队列只有一个线程操作。

下面是具体代码,代码量很少。

channel.h:
定义基本数据结构以及接口

#ifndef _CHANNEL_H_
#define _CHANNEL_H_

#include "pv.h"

typedef struct
{
    int count;   // allocate count
    int used;    // used count
    int cursor;  // current position to rcv
    int last;    // current position to send
    int valsize; // each element size
    void *data;  // data memory ptr

    mutex_t lock; // mutex lock

} channel_t;

channel_t *new_chan(int valsize);

// send val to channel
// if channel is full, expand it, the expand strategy is old_size * 2
int chan_send(channel_t *chan, void *val);

// recv value from channel
// return !0 if channel empty
// otherwise return 0 and update the element params
int chan_rcv(channel_t *chan, void *ele);

// free queue data
// free channel
void free_chan(channel_t *chan);

#endif

channel.c:
接口实现


#include <stdlib.h>
#include <string.h>

#include "channel.h"

channel_t *new_chan(int valsize)
{
    channel_t *ch = malloc(sizeof(channel_t));

    if (!ch) {
        return NULL;
    }

    ch->count = 1;
    ch->valsize = valsize;
    ch->cursor = 0;
    ch->last = 0;
    ch->used = 0;
    ch->data = malloc(valsize);

    mutex_init(ch->lock);
    return ch;
}

// send val to channel
int chan_send(channel_t *chan, void *val)
{
    if (!chan) {
        return -1;
    }

    P(chan->lock);

    if (chan->used == chan->count)
    {
        int old = chan->count;
        int newcount = old * 2;

        chan->data = realloc(chan->data, chan->valsize * newcount);
        if (!chan->data)
        {
            V(chan->lock);
            return -2;
        }

        // reconstruct queue
        // supporse that the old channel is data[1,2,3,4,5]
        // the cursor pointer is 3, the last pointer is 2.
        // after expand, the channel is data[1,2,3,4,5,0,0,0,0...]
        // it should be data[0,0,3,4,5,1,2,0,0,0...]
        // the last pointer should be cursor + old_queue_size
        memcpy(chan->data + old * chan->valsize, chan->data, chan->cursor * chan->valsize);
        chan->count = newcount;
        chan->last = chan->cursor + old;
    }

    memcpy(chan->data + chan->last * chan->valsize, val, chan->valsize);
    chan->last = (chan->last + 1);
    if (chan->last == chan->count)
        chan->last = 0;
    chan->used += 1;

    V(chan->lock);
    return 0;
}

// recv value from channel
int chan_rcv(channel_t *chan, void *ele)
{
    P(chan->lock);
    if (chan->used == 0)
    {
        V(chan->lock);
        return -1;
    }

    memcpy(ele, chan->data + chan->cursor * chan->valsize, chan->valsize);
    chan->used -= 1;
    chan->cursor += 1;

    if (chan->cursor == chan->count)
    { // cursor move the begin of the queue
        chan->cursor = 0;
    }

    V(chan->lock);
    return 0;
}

// free channel
void free_chan(channel_t *chan)
{
    mutex_destroy(chan->lock);
    free(chan->data);
    free(chan);
}


pv.h:
锁操作的包裹函数

#ifndef _PV_H_
#define _PV_H_

#include <pthread.h>

#define mutex_t pthread_mutex_t

#define mutex_init(mu) pthread_mutex_init(&mu, NULL)
#define mutex_destroy(mu) pthread_mutex_destroy(&mu)
#define P(mu) pthread_mutex_lock(&mu)
#define V(mu) pthread_mutex_unlock(&mu)

#endif

接下来用图片显示具体每个操作之后内存状态。

  • 初始化
    分配一个元素的空间
    image

  • chan_send p1
    不需要扩容,直接附加到last即可
    image

  • chan_send p2
    send p1之后,data指向的内存块已被占用完。需要进行扩容,然后再附加到last
    image

接下来如果继续send,仅仅是重复上面两个过程,容量充足,则附加到last,不足,扩容再附加。

但是通常不会出现一直send的情况,会在send和rcv之间交替进行。
假设 send p1和send p2之后执行recv操作。

image

稍稍有点奇怪,但是也还算正常,这时候如果再 send p3, send p4。

image

这里挺巧妙的,需要琢磨一下,为了保证先进先出,需要将cursor之前的数据,也就是后进的数据移动到后面。代码当中也写了很长一段注释说明。

        // reconstruct queue
        // supporse that the old channel is data[1,2,3,4,5]
        // the cursor pointer is 3, the last pointer is 2.
        // after expand, the channel is data[1,2,3,4,5,0,0,0,0...]
        // it should be data[0,0,3,4,5,1,2,0,0,0...]
        // the last pointer should be cursor + old_queue_size
        memcpy(chan->data + old * chan->valsize, chan->data, chan->cursor * chan->valsize);
        chan->count = newcount;
        chan->last = chan->cursor + old;

总的来说很小巧,不难,又有意思,适合无聊逛github的时候看看。

一个非常简洁的内网穿透实现

首先打广告,内网穿透Notr,网站部分功能仍在开发,如果您也有兴趣参与Notr的开发,请给我留言。

在完成大部分gtun 的开发之后,已经能够解决将家里网络与公司网络组建成虚拟局域网,面临一个问题,我需要在两端同时gtun客户端方能连接公司的网络,于是就想到了内网穿透,基于gtun可以非常方便的进行内网穿透。在gtun当中也有部分内网穿透的功能,但是还不够,或者说我不认为将内网穿透加入gtun让gtun变得庞大无比是一件正确之举,于是我在gtun基础之上构建一个内网穿透的项目。gtun作为底层基础设施。

当时思考了下,应该具备以下功能:

  • 首先,地位应该是一个产品而不是一个开源项目,所以给用户使用的时候一定要考虑小白的情况,所以不需要众多的配置,只需要让用户知道,我本地起了什么端口,我只要告诉你我起了什么端口,你帮我让我能够通过公网访问就OK,所以Notr的客户端非常简洁,没有任何配置,只需要指定本地端口即可

  • 其次,应该支持多种协议,HTTP,HTTPS,TCP,UDP

  • 最后,用户最终拿到的应该是域名而不是IP地址,如果是IP地址可能会经常变动,但是域名不会

于是开始了Notr的思考与实现。最初版本Notr针对HTTP和HTTPS是需要指定端口号的,朋友反馈基本上两个问题用得不舒服

  • 端口号随机的,而且每次都不一样。产生这个的原因,是自身程序是不关注应用层协议的,只关注TCP层,而且我会都会监听一个端口来服务这一代理协议

  • 其次,HTTPS安全连接的问题

于是用HTTP和HTTPS用Nginx反向代理代替,每次新生成一个Nginx配置文件,proxy_pass指定为用户的虚拟ip地址与端口即可。

这种方式实现将会特别简洁,而且会http可以使用默认的80端口,https可以使用默认的443端口,用户不需要记住端口号,仅仅需要记住域名。

Notr的运行截图如图所示:

cbc9cd87-93fc-4a0c-862d-a46d45eac89a

对小白用户非常得友好。没有任何配置。目前支持多个windows,linux以及mac三个平台,不过windows需要安装tap-windows驱动,更多信息可在notr上了解,可执行文件可在下载中心进行下载

详细处理流程:

image

ip地址经纬度查询

趁着过年有空闲时间做了个小项目,其中有一小部分功能是网络流量可视化,能够在世界地图上显示网络流量的情况,这里面需要解决一个问题,就是根据网络ip地址能够定位到具体的经纬度,经过一番查询,最终决定用 freegeoip 的api来进行。百度的ip地理位置有数量限制也是就舍弃了。但是用了百度的echarts作为流量显示组件。目前效果大致如下。

image

工作总结(2016-2018)

眨眼之间,从大学毕业至今已将近两年,两年时间从学生转变成一个可以适应职场生活的上班族,经历了很多人,很多愉快的不愉快的事,自身也发生了非常多的转变,但是似乎没有做过一次非常系统的总结,特此一文,总结一些职场当中的收获。

首先是非技术方面,在创业公司待久了之后会发现其实大部分创业公司死的原因其实在于管理,大部分员工跳槽最主要的原因是上司,而管理是非常考验人的管理理论,情商,观察力,分析力以及沟通能力的一项技能,** 从而提高下属的工作兴趣,工作效率以及降低其离职率 **,感觉又非常的复杂,有意思。所以在非技术方面,我会回顾自己经历的上司,自己一些失败的经历来叙述每一个总结点

不要跟风喷产品经理或者游戏策划

这个被我列在首位,因为我曾经也跟风喷过策划。很多新手程序员(当然我也还算在新手村里面待着的),在没进入这个行业之前可能听过很多关于程序员和产品经历之间的各种段子,于是对产品经历产生了抵制,跟着转发一些趣图,段子之类的,这是非常不可取的,我还记得我刚毕业的时候微信头像是一群人围着策划开枪的图片,现在想想真是无知,你要知道,在工作当中,做产品的和做研发的很有可能会频繁的交互,沟通协调,如果带着一种消极的心态,是非常不利于沟通的,而且,试问和你沟通的你口中所谓的“产品狗”,如果看到你朋友圈转发的趣图,他会觉得有趣吗?很多问题都是可以进行友好的沟通解决的,要认真听取和分析,不合理或者有分歧的地方可以再讨论,很多东西都是可以讨论进行解决的,只要具备足够耐心。

做好一个倾听者

倾听应该算是我个人最看重的一点,(其次是谦虚,这两点应该算作一点了),我刚毕业的时候在一家游戏公司待了一段时间,当然最后选择了离职,离职原因不在公司,最主要的是我个人身体问题,加班实在是太严重了,我这边身体完全吃不消。但是当时的上司人非常的好,非常赞的一个leader,我现在很多观念应该都是受他的影响,其身上最出色的有点应该就是会聆听,人与人之间说不通话我个人认为最主要的原因是他会认认真真等你把话说完,不要以为这是一件很简单的事,实际上,这非常的困难,这是非常高情商的一种表现,至少后面我经历的大部分同事都存在打断别人话,他们或许认为已经理解对方要表达的意思了,其实并没有的,最后还需要手动说一句,你先听我把话说完,但是即使是这样子,还是会存在其他问题的,比如说,对方很有可能会认为你听了他插入的描述之后立马转变自己的枪口,听你把话说完。所以说去完完整整听对方把话说完,这个本身就并非一件容易的事了。很多人之所以成为不了一个领导者或者一个好的领导者,这项能力是非常重要的,你要做不好,真的就别怪下属跳槽。就我个人而言,无论是工作还是生活当中我都是一个非常安静的一个人,以至于很多人觉得我很闷,和不合群,半句话不离工作,大概是我个人性格的一项重大问题,这点想修正其实非常难,除非人生再经历非常大的波折,想想还是不要了,哈~

注重Team Work

这里记录一个我前上司的故事,前上司,也是我目前公司的前CTO,个人能力挺强,可能跟人生经历有关,作为it届的老人,我很尊重他,跟他沟通能够了解到整个行业变化的历程。但是仅限于此,要想一起出去创业之类的,我绝对会说NO的,倒不是我现实,只是我不太喜欢跟个人英雄主义太强的人合作,如果我是一位领导者,我二话不说要开的肯定是那些无法跟同级成员合作的下属,即使你非常的聪明,我觉得并不缺乏聪明的人,但是我缺乏能够一起做事的人。而且回过头想想,真的有这么多聪明的人吗?不见得的。

在此我表明我个人关于聪明的两个观点
1)如果你是上司,不要公开说自己某个下属非常聪明,没有好处,只有坏处
2)如果你觉得自己挺聪明的,大家好像也都是这样觉得,请收好这颗心,他很有可能会害死你

不要自己做一个项目

如果一个项目是需要自己一个人做的,我会立马跟上司表明我的观点,这个非常不利于团队协作能力,作为上司,也尽量不要安排人员独立完成项目,很有可能项目完成之后你会失去一个同事。

如果你是一个领导,记得跟底层员工多交流

尤其是在创业公司,很多创业公司的老总非常的有人脉,想法有行动力甚至资金,但是最后却失败,回头想想为什么,或许你怎么也想不明白,但是换个角度去思考,你的员工肯定知道,他们一天到晚就待在公司,跟公司的人接触,跟公司的项目接触,跟客户接触,他们有非常切实的感受,肯定有很多员工是非常了解企业的问题和不足,但是他们不会跟你说,你要记住这点,他们很多是不会跟你说的,第一点你不是他们的朋友,第二点他们怕得罪人,毕竟他们也只是混口饭吃而已,第三点他们为什么要告诉你,这点很可笑,但是会存在,简单来说他们本来就是基层,随时都可以换份工作,这对他们来说是工作不是事业,他们很明确的知道自己是来学习技能和积累经验的,你公司发展怎么样他根本就不关心,他们接触不到boss这一级别的,boss对他们来说是透明的,从入职到离职,都没有和boss有过任何交流,怎么可能会突然敲你门然后给你提意见。或许离职的时候会稍微敞开一些跟你表明,但是要知道,离职很多人也是追求你好我好大家好,凡事留一线日后好相见的态度。如果不常跟他们交流,你也许根本就不会知道也许你自己公司内部已经糟糕到不能再糟糕了,很多员工都在准备跳槽,当新的一波招聘过来的时候,也许你回头发现公司已经所剩无几了,这时候真的就无力回天了。

管理人事一件非常困难的事

人员用不好,那整个公司就完来,技术选不好还有得补救,中层领导选错了,那就别忙活了。因为你会收到来自中层领导的错误的讯息,这些错误的讯息会引导你错误的看待事情,我记得在本科一门管理学课程上有过一个案例,大致意思应该是一句话从底层员工,到经理到总监最后再到总经理,整个过程当中每一步都会有存在消息的遗漏或者出于其他目的替换,最终消息到达总经理手上完全是另外一个描述了。我本科获得的是管理学学位,所以我对管理的带来的重大意义会有更多的一些感受。

非技术上还有非常多的一些体验,都是从我自己身上,从同事身上,从上司身上看到的优缺点,其中负能量居多。在技术上,体会尤其深刻

先做好工作,再想其他

很有幸在碰到一些认为工作当中无事可干的朋友,慢慢沟通会发现,其实他们并非无事可干,只是不喜欢干工作项目,觉得没有意义,学不到东西,浪费时间,再进一步沟通,你又会发现,他们本身工作范围内的项目其实也没有做好,然后又想做一些自己感兴趣的,觉得牛逼的事。但是相信我,他们很快又会对他们之前觉得有意义的事失去兴趣,这一现象在IT行业有一个出现频率非常高的词,叫** 浮躁 **。

工作是你最直接的面对的事,会遇到很多的问题,而且这些问题都非常考验解决问题的能力,最主要的是,你工作的产出会直接到用户/客户手上,他们会反馈消息,你需要根据反馈进行调整和优化,当一切安静下来时,也许你会想到以前哪里的实现不太好,存在某种妥协,这时候就会回头考虑重构或者调整,这里面有非常多的学问,即使当你重构完成之时,你也会发现你工作用到很多技术你没有很深刻的理解,以后如果有坑了可能无法快速填好,这时候你可能会考虑阅读相关的源代码,而阅读源代码问题又来了,很多项目的源代码需要很多数据结构的知识,操作系统和网络通信相关的知识,如果这些背景知识你没有,你很有可能会需要花点时间去学习。

单单这一系列的描述都已经很长了,怎么可能无事可干。

该用C的时候用C,该用go的时候用go,该用python的时候用python

编程语言之争没有意义,我在校和老师做项目用Java和R,后面转C,和实习用的是C,后面转go,现在用go,但是涉及到底层的我们依旧会选择C。语言够多了吧,但是没啥意义,大部分语言的基本使用都用不了两个星期,实际上我从c转向go只用了一个星期不到,当时还是个学生,还没毕业呢。所以我其实是特别不乐意跟别人讨论 go里面可以XXXX吗,go里面也可以怎么怎么样啊?为什么不用C写呢,效率多高?为什么不用Python呢,python多么优雅?之类的问题。

对技术要客观,不要跟风

现在的IT行业不懂怎么说,有点娱乐圈的感觉,关于程序员,产品经理,运营,美工之类的段子特别多,各种技术也层出不穷,各种玩味很重的图也很多,朋友圈也频出各种技术文章,我也都会看看,真的,我都会看看,但是大学教会我一点东西就是听完别人的话,看完别人的描述,再分析判断他说/写的是不是合理的,你不要以为博客上面的文章都是合理的,正确的,并不是的。最重要的是要有有自己的判断力,靠博客或者视频来获取观点的其实我个人认为不太好,我会系统的阅读书籍,有相关背景知识,然后从头到尾看其变化,理解为什么会发展成这个样子,最后会得出当前还有什么不足需要解决的这一大boss,然后才会有相关文章是不是真的提供有解决这一问题的方案。这个感觉跟做项目很类似,你是可以很清楚的看到当前项目用到的一些技术,但是这没啥意义,你要清楚为什么人家会采用当前的方案,他们经历过那些心路历程,有过哪些调整和优化,然后再看看当前项目还有哪些不足,才能进行接下来的工作。

抛掉学生气息

我至今还有非常高的学术气息,在遇到问题的时候,我很有可能是去找论文而不是网上搜索答案,然后在看论文的过程中,其实你会发现,也许很多所谓的新技术,新理念,学术圈可能好几年前就已经有一些论文了。但是最近慢慢意识到,很多刚毕业不久的同学学生气息非常重,完全没有转换角色,具体表现有几点

  • 喜欢看书,喜欢解ACM题目,上班时间
  • 喜欢看数学题
  • 算法是不错,但是你要给他建好模,将实际问题抽象成一个算法问题,他才懂做
  • 没有deadline的观念
  • 代码写起来特别的随意,而且很不可靠,不靠谱

其实还有很多问题,都是在我自己身上和同事身上看到的。

加速器的一些理解

面对的问题

在访问一些海外网站或者海外应用时,通常会遇到访问慢的情况,访问慢有两个原因,一个是丢包,一个是线路拥塞。当然还有其他一些原因,比如说运营商劫持或者防火墙拦截的情况。当你在跨国企业时,这种现象就特别严重,比方说,你们公司的数据中心在美国,而你的客户端在需要与数据中心交互获取最新的数据,这时候再丢包就影响心情了。所以,很多时候,我们会加个类似”跳板“之类的东西来加快我们与数据中心的交互速度。

加速器

很多人理解的加速器仅限于网游加速器,简单来说是玩外服游戏时特别卡,丢包严重,延时严重,需要一款加速器来让我们游戏数据走加速线路出去,而加速线路给他们解决来丢包和延时的问题。但是我认为加速器应该有更大的用途,可以加速应用,游戏也是应用的一种,当然也访问一些在国内无法访问的网站。

加速器原理

目前了解到的所有加速器基本都是一个原理,将客户数据通过一条高速通道把数据流引到某台服务器A,A与最终需要访问的应用应该足够稳定,丢包和延时都比较好,然后A再将用户的网络数据包发送给目的服务器,并且将结果原路返回,最终回到客户电脑,简单来说中间整个过程都是 代理,把数据送出去再送回来,参考下图。

image

这里面的难点都在tunnel上,而tunnel上又可以继续细分下来。

在了解tunnel之前,需要知道两点
1)tcp协议在长距离传输丢包会非常严重,从大陆地区到香港某些时刻会丢包丢到怀疑人生
2)运营商对udp数据有歧视,参考udp2raw

之前提到,tunnel的目的就是将数据送出去再送回来,那么会碰到第一个问题,数据从哪里来。

因此,我们需要采取一种措施将数据导向我们tunnel的入口。措施有很多种,每个系统都不太一样,我接触的大致会有一下几种方式

第一种,通过网关的方式,所有数据都会经过网关外出,如果你有一个硬件,可以是树莓派或者其他,接入正在使用的网络,然后开放ap或者手动设置网关或者dhcp的方式,接入的所有设备的网络数据包自然就会到达网关来。

image

这个图片非常抽象,我觉得需要加以说明。首先我们的设备gateway(这里假设是树莓派)和用户接入共同的网络,可以认为他们连上同一个热点,在一个局域网里面,但是,修改pc的网关指向我们的树莓派,这样pc上的网络数据就会经过我们的树莓派出去,我们就可以在树莓派上对数据包进行处理,决定哪些数据包应该进行何种操作。这是一种通过网关的方式。

第二种是编写客户端,这种很容易理解,每个pc上运行有一个客户端,客户端可以决定哪些数据包应该进行何种操作,实际上,第一种方式也可以认为是一种客户端,但是同时也是pc的接入点,他可以接入多个pc的请求,相当与路由器。具体如下图

image

这其中pc出口后面的线路省略。

还有第三种,不用编写客户端,也不用搞树莓派,通过vpn+接入程序。好处是不用额外的设备,也不用针对各个平台开发客户端,只有云服务器,缺点是如果vpn使用系统自带的,如果走的是udp协议,在运营商这块可能就会被卡住,还有就是可能会牺牲一点性能。

image

这三种引流方法相对比较容易实现的是第一种,当然第一种实现了之后,第三种基本上就实现了,第二种由于平台之间的差异性,实现难度不小的。还有其他方式可以进行引流,可以设置pac和一些类似socks之类的代理,但是无法做到全局代理,如果某个应用不走域名,直接走ip,而且走的是tcp或者udp协议,那么这类应用将无法加速。

接下来针对这三种方式的一些技术内部做一些描述

网关设备

image

既然我们已经在开发跑在设备之上的网络程序了,那我们在设备之上肯定会选择linux,一方面是Linux在网络方面能够进行更多的控制,还有就是用户态的工具和库比较多。

在pc配置了网关指向之后,我们需要在网关上做分流的工作,让部分流量走我们的tunnel方向出,部分流量走用户的正常网络出,有两种方法可以做到这种

首先第一种是非常普遍的做法,采用虚拟网卡的方式,也就是tun和tap两种类型的设备,关于tun和tap设备的基本原理也是需要很大篇幅才能够描述清楚,这里只需要知道,tun设备可以接收route过来的三层数据包,读取tab设备可以读取到二层数据帧,剩下的就是把三层包包装发送出去,走tunnel。这是目前大部分加速器都会采用的方式,简单,稳定。值得注意的是,windows平台下虚拟网卡设备是需要单独开发驱动程序的,所以相对来说windows下通过tun和tap实现加速器的难度要更大,linux内核已经有虚拟网卡驱动程序了。windows下还有其他更加恶心的机制也能够实现分流。关于tun设备,这里有个参考资源

第二种做法相对来说可能比较少见,通过NAT的方式,本地监听tcp和udp端口,符合条件的数据包将通过DNAT来将数据导向我们本地监听的端口,所有分流都是通过nat来实现,以tcp为例,也就是说用户实际上是和我们本地的程序建立三次握手和数据交互。我们程序可以读取用户的数据,将数据通过tunnel再发送出去,这里面有个问题,无论如何我们的A程序都需要知道这些数据该送去哪里,所以在网关这里需要把用户的这边原本想访问的真实地址发送给A,而问题就是网关这边是经过DNAT的,对这个tcp连接来说,他能拿到的目的地址就是他本身的ip以及端口。所以需要一种机制能够将用户真实想访问的地址和端口给查询出来,再发给A节点。

Linux内核里面有一个连接跟踪的概念,能够标示某个连接的双向五元祖,即是
原始五元祖
OriginSrcIP,OriginSrcPort,OriginDstIP,OriginDstPort,OriginProtocol

返回时候的五元祖
ReplySrcIP,ReplySrcPort,ReplyDstIP,ReplyDstPort,ReplyProtocol

我们需要的信息就是OriginDstIP和OriginDstPort,

接下来问题就是怎么查了,连接跟踪是存放在内核里面的,也就是说我们得想个办法和内核通信,于是就回到了用户态和内核态通信方式上。我了解的通信方式有几种

一种是netlink方式,很久之前的一段代码里面似乎有使用netlink往内核模块发送配置消息的

一种是设备文件方式,之前的tun和tap就属于设备文件的一种

当然还有其他方式,我用过的基本就这两种。

如果从内核到用户态的话,netlink就相当的挫,但是使用netlink给内核点消息这个完全是没问题的。netfilter有个工具可以查看连接跟踪的,叫conntrack,仔细琢磨netfilter的官网会发现conntrack用了他自己写的一个库,叫libnetfilter_conntrack,就我接触netfilter来看,netfilter用户态下所有的配置工具都是采用netlink的方式。但是在用这个之前得考虑性能上的问题。

设备文件基本上就是要写内核模块,然后把所有连接信息写入到某个文件里面去,然后用户进程在去读取这个文件,然后进行解析。

这是网关方式的基本流程,基本逻辑如下

func main() {

	tcpAddr, udpAddr := ":2000",":2000"
	ListenTCP(tcpAddr)
}

func ListenTCP(tcpAddr) {

	listener, err := net.Listen("tcp", tcpAddr)
	if err != nil {
		return
	}

	for {
		conn, err := listener.Accept()
		if err != nil {
			break
		}

		go handleTCPClient(conn)
	}
}

func handleTCPClient(conn net.Conn) {

	originDstAddr := getOriginAddr(conn.LocalAddr().String(), conn.RemoteAddr().String())
	// forward to tunnel

	// write back to client
}

具体实现起来还是有一些难度的。

客户端

第二种客户端方式在Linux下实现难度相对比较小,其他平台具体实现的机制似乎都不太一样,以Windows平台下为例,windows平台也可以通过虚拟网卡+路由的方式实现,但是windows应该是没有实现虚拟网卡驱动,怎么办,估计得自己去写一个虚拟网卡驱动,人都不是万能的,我相信windows平台下这方面的人比Linux来说要少得多。我一开始了解windows平台下客户端的实现时,并没有从虚拟网卡的角度去实现,当时的基本思路如下:
根据在学校接触到的win32汇编的经验可以快速想到windows平台下是可以hook住某个api然后注入函数的,所以我想能不能hook住connect函数,然后把目的地址改成本地的地址,这样数据包出去的时候就会往本地走来,但是这种注入方式应该是需要知道进程id,不太友好,所以就不做细想,后面了解到windows平台下提供有一种叫做lsp的机制能够实现相关需求,再进一步了解,发现lsp机制已经让微软给淘汰来,取而代之的是一种叫wfp的技术,这块在网络安全方面用的应该是比较多,但是他们的对于我而言,缺点都是要写驱动相关的代码,所以就放到来待定的位置来,后面参考openvpn的实现方式,发现用的是虚拟网卡,ok,跟Linux下的实现类似,但是这个网卡驱动,估计得自己写。所以私以为客户端的方式不太适合linux程序员,当然如果本身是个开发团队的话,可以试一试。

vpn+云服务器

采用vpn+云服务器的方式最简单方便,通过vpn将流量导入到云服务器,然后再在云服务器上实现分流,这种方式基本综合了前面两种实现,并且服务器性能通常比设备性能要高,这里面有几个问题。
第一,如果vpn采用udp的方式运行,小心运营商阻断导致丢包,第二,客户电脑上可能运行了其他程序,特别是杀毒软件之类的,其他程序有可能会造成一些干扰,比如说,如果你需要加速dns,但是某些杀软可能非常“友好”的给你优化了dns,将DNS指向他们自己的DNS,这个就会有问题。第三,由于不在客户端进行分流,所以所有流量都会到服务器,这个就导致,如果服务器带宽比客户上的带宽要小,那么客户不经过加速的地址的下载速度可能就会因服务器带宽较低导致速度变慢,客户那边就会call你了。所以可能云服务器需要配置非常高的带宽,带宽通常都挺贵的。

tunnel广域网

再来研究研究广域网部分,也就是tunnel里面的技术,这块我目前涉及不深,以下面模型为例

tunnel

大部分开发人员都知道,TCP协议本身很复杂,但是复杂说明他替你做了很多事,比如自动重传,拥塞控制以及滑动窗口等技术,但是tcp协议又太复杂了,长链路tcp丢包重传又很严重,UDP协议就比较简单,源端口目的端口各占两个字节,长度和校验和各占两个字节,就八个字节udp头,其他什么都没有,但是相对而言用udp实现一些需要可靠性的就会相对复杂,目前业界有两款用的应该是相对比较多的协议,一个是google涉及了quic协议,quic协议是基于UDP的,另外一款是kcp协议。当然还有其他的很多,我认为可以从学术的角度进行搜索,学术界肯定有过很多尝试和发表过的论文可以参考。解决的问题都是用udp实现相对可靠的传输。

Tun/Tap设备基本原理

接触过VPN相关技术的基本都会接触过虚拟网卡,tun,tap等字眼,因为大部分vpn都或多或少使用有类似技术。本文会对tun/tap设备的基本原理进行说明,并且对其如何应用在vpn上进行了分析,最后提供一个简单的tun的vpn的实现代码。

TUN/TAP设备的基本原理

首先需要明确一点,tun和tap是两种类型的虚拟设备,其一大区别是从tun设备读取数据,你将能够拿到三层包,从tap网卡获取数据,你将能拿到二层包。

在了解虚拟网卡之前,应该先简单了解下真实网卡是如何进行工作的。
首先,网卡介于物理网络和内核协议栈之间,接受协议栈外出的数据并将数据往物理网络发出,同时,也接受外部数据并交付给内核协议栈进行处理。(在这里先将内核协议栈当成一个整体,一个黑盒来看待。)

了解物理网卡所处的位置以及网络数据包的流动之后,再看看虚拟网卡有什么不一样的地方。
从最直观的使用来看,用户是可以直接读写虚拟网卡的,也就是说,从内核协议栈发出的数据在选定以虚拟网卡发出之后,数据将会被用户层程序直接读取,这点与物理网卡不一样,物理网卡直接就往外发。虚拟网卡告知用户程序数据可读。

在写方面,用户进程往虚拟网卡写数据会直接从网卡写出去
一图胜千言:

image

TUN/TAP与VPN

了解了TUN与TAB的基本原理之后,可以明确的知道,用户层通过虚拟网卡具备有读写二层,三层数据包的能力,这种读写与原始套接字还不一样,原始套套接字做的事旁路拷贝,这个是直接截取数据包到用户层,用户层自己处理。

有了这类技术底子之后,再看看vpn,很多人一提到vpn就想到翻墙,vpn并不等于翻墙,vpn的一个目的是为不同地区模拟出一个局域网环境,让A地区的员工能够像访问局域网一样访问位于总部B的服务器或者其他比如打印机,这是vpn。

一图胜千言:

image

ping经过内核协议栈,路由选择从虚拟网卡发出

虚拟网卡的另外一端,也就是用户进程,将这一ping包读取出来

将ping的payload通过真实网卡发出,经过一系列的传输,到达目的主机,

目的主机收到数据包之后,将其写入虚拟网卡。

Ping reply返回类似,上图的左右两端是等价的,能够收发数据包。

为了方便说明这一原理,编写一个简单的基于tun设备的vpn——gtun

gtun客户端:

package main

import (
	"encoding/binary"
	"flag"
	"net"
	"os"
	"os/signal"
	"syscall"

	"github.com/ICKelin/glog"
	"github.com/songgao/water"
)

var (
	psrv = flag.String("s", "120.25.214.63:9621", "srv address")
	pdev = flag.String("dev", "gtun", "local tun device name")
)

func main() {
	flag.Parse()

	cfg := water.Config{
		DeviceType: water.TUN,
	}
	cfg.Name = *pdev
	ifce, err := water.New(cfg)

	if err != nil {
		glog.ERROR(err)
		return
	}

	conn, err := ConServer(*psrv)
	if err != nil {
		glog.ERROR(err)
		return
	}

	go IfaceRead(ifce, conn)
	go IfaceWrite(ifce, conn)

	sig := make(chan os.Signal, 3)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGABRT, syscall.SIGHUP)
	<-sig
}

func ConServer(srv string) (conn net.Conn, err error) {
	conn, err = net.Dial("tcp", srv)
	if err != nil {
		return nil, err
	}
	return conn, err
}

func IfaceRead(ifce *water.Interface, conn net.Conn) {
	packet := make([]byte, 2048)
	for {
		n, err := ifce.Read(packet)
		if err != nil {
			glog.ERROR(err)
			break
		}

		err = ForwardSrv(conn, packet[:n])
		if err != nil {
			glog.ERROR(err)
		}
	}
}

func IfaceWrite(ifce *water.Interface, conn net.Conn) {
	packet := make([]byte, 2000)
	for {
		nr, err := conn.Read(packet)
		if err != nil {
			glog.ERROR(err)
			break
		}

		_, err = ifce.Write(packet[4:nr])
		if err != nil {
			glog.ERROR(err)
		}
	}
}

func ForwardSrv(srvcon net.Conn, buff []byte) (err error) {
	output := make([]byte, 0)
	bsize := make([]byte, 4)
	binary.BigEndian.PutUint32(bsize, uint32(len(buff)))

	output = append(output, bsize...)
	output = append(output, buff...)

	left := len(output)
	for left > 0 {
		nw, er := srvcon.Write(output)
		if err != nil {
			err = er
		}

		left -= nw
	}

	return err
}

gtun_srv,中间转发服务

package main

import (
	"io"
	"net"

	"github.com/ICKelin/glog"
)

var client = make([]net.Conn, 0)

func main() {
	listener, err := net.Listen("tcp", ":9621")
	if err != nil {
		glog.ERROR(err)
		return
	}
	for {
		conn, err := listener.Accept()
		if err != nil {
			glog.ERROR(err)
			break
		}

		client = append(client, conn)
		glog.INFO("accept gtun client")
		go HandleClient(conn)
	}
}

func HandleClient(conn net.Conn) {
	defer conn.Close()

	buff := make([]byte, 65536)
	for {
		nr, err := conn.Read(buff)
		if err != nil {
			if err != io.EOF {
				glog.ERROR(err)
			}
			break
		}

		// broadcast
		for _, c := range client {
			if c.RemoteAddr().String() != conn.RemoteAddr().String() {
				c.Write(buff[:nr])
			}
		}
	}
}

这里示例程序为了简化Demo,中间转发服务器将收到的数据包广播给所有的客户端,具体gtun实现当中会有一个协议的解码,根据目的地址来做转发。

后续将会往路由选择方面靠拢,逐步将内核协议栈这一黑盒慢慢打开。

IPV6访问环境

上周需要针对当前版本的加速隧道添加ipv6的支持,这也特别符合我们程序的理念,我们程序就是一个隧道,所以如果隧道起点和终点协议不需要进行调整,那么就需要在起点与终点处添加对ipv6的访问支持即可。事实上只需要在隧道的终点添加ipv6的支持即可。

目前国内运营商不支持ipv6,需要访问ipv6地址需要另外进行处理,在内网环境可以通过一以下方式进行。

1. sudo apt-get install miredo 
2. 以root权限打开ufw的配置文件: “/etc/default/ufw”,找到”IPV6=no”这一行,将其改为”IPV6=yes”。 
3. sudo invoke-rc.d networking restart

在公网当中,可以通过如下方式进行ipv6参考阿里云ipv6

连接跟踪的一种非常暴力的获取方式

在项目当中有时需要连接跟踪相关的信息,netfilter官网提供了一个libnetfilter_conntrack的用户态库,通过netlink的方式与内核对应的模块通信操作conntrack,但是会有性能瓶颈,还有就是问题不好定位,本文提供一种用户层获取连接跟踪的实现思路,在继续阅读之前,请确保具备以下基本知识:

  • 了解netfilter
  • 能够开发基于netfilter的简单的包过滤防火墙
  • 对skb有一定的了解

解决思路很清晰,在内核层面netfilter钩子当中hook数据包,然后强行在L4和payload之间封装一层协议,有点类似运营商植入广告,将连接跟踪的信息通过这层协议传递给用户态,用户态先解这层协议。再读取数据。

比如说,用户层比较关心DNAT之前的目的地址(origin_ip)和目的端口(origin_port),那么可以将origin_ip和origin_port再加上payload的长度三个字段作为一层协议,插入到tcp和payload之间。这样用户层的应用只需要正常读数据,读取协议头,再读取payload即可。

以TCP为例,部分示例代码如下:

// tcp
static int handle_tcp(struct sk_buff *skb){
	struct iphdr *ip = ip_hdr(skb);
	struct tcphdr *tcp = tcp_hdr(skb);
	
	// conntrack
	enum ip_conntrack_info conntrack_info;	
	struct nf_conntrack_tuple *origin_tuple = NULL;
	struct nf_conn *ct = NULL;

	// inject buffer
	char insert_data[8];
	char *ptr = &insert_data[0];

	// origin payload
	char *origin_data = skb_network_header(skb) + ip->ihl * 4 + tcp->doff * 4;
	int origin_len = skb->tail - skb->network_header - ip->ihl * 4 - tcp->doff * 4;

	ct = get_conntrack(skb, &conntrack_info);
	if (ct == NULL) {
		return NF_ACCEPT;
	}

	origin_tuple = &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple;

	// force accepted pkt
	if (is_accepted_pkt(origin_tuple, ip->daddr, tcp->dest) != 0) {
		return NF_ACCEPT;
	}

	// empty payload
	if (origin_len == 0) {
		return NF_ACCEPT;
	}

	// modify tcp payload
	ptr = encode32u(ptr, origin_tuple->dst.u3.ip);
	ptr = encode16u(ptr, ntohs(origin_tuple->dst.u.all));
	ptr = encode16u(ptr, origin_len);


	// expand skb tailroom
	if (skb_tailroom(skb) < 8) {
		if (expand_skb(skb, skb_headroom(skb), skb_tailroom(skb) + 40) != 0) {
			return NF_DROP;	
		}
	}

	// copy conntrack data into skb payload 
	memmove(origin_data + 8, origin_data, origin_len);
	memcpy(origin_data, &insert_data[0], 8);

	nfct_seqadj_ext_add(ct);
	__nf_nat_mangle_tcp_packet(skb, ct, conntrack_info, 
			ip->ihl * 4, 0, origin_len, 
			origin_data, origin_len + 8, true);

	return NF_ACCEPT;
}

在用户态先读八个字节的协议,将origin_ip和origin_port以及payload长度length读取出来,然后再读取length字节的数据。不需要任何库,难点在于编写一个稳定的内核模块。

TCP/IP 网络层

网络层非常复杂,想通过这篇文章完全了解网络层是不太现实的,本文只是描述网络层的解决的一些问题,以及它具体是怎么解决的描述清楚。

** 网络层解决的一个重要问题是如何将数据包送到目的地。**

网络层的互联通过路由器进行,路由器接收下级网络的数据,并转发到上一级(如果需要的话),这个属于路由转发,路由转发解决将数据包转发出去,但是在做路由转发之前,需要确定转发到哪个地址,只就需要路由器内部的路由表,如果目的地址在当前网络下,那就不需要转发给下一跳,否则获取到下一跳地址然后转发给下一跳,这里面又会引出另外两个问题,第一,怎么判定是否属于同一个网络,第二,路由表怎么生成,生成的依据是什么,这里面可能又需要涉及生成路由表的路由算法。每个路由器都需要做路由表匹配以及转发到下一跳的过程,但是路由算法并不需要每个路由器都运行,上述过程可以概括为以下流程。

image

所以要实现路由器的转发功能,需要解决几个问题:

  • 如何确定是否属于同一个网络
  • 如何生成路由表
  • 如何匹配路由表
  • 如何转发给下一跳

判定两个地址是否属于同一个网络

如何生成路由表——路由算法

路由匹配

路由转发

如何小成本监控个人开发的应用

现在很多人有时间都希望写一些自己的代码,把自己的一些想法给实现一下,在实现自身想法的过程当中,可能会忽略一些基础组件的重要性,我之前也是非常不重视基础组件,然后一箩筐代码写下来,写完了,后面再来开发一些基础组件,改动还是挺大的。

在开发notr的过程当中,前期专注软件的实现,经过一波又一波的优化,软件功能基本满意之后,又想着如何去把软件介绍给别人使用,依然还记得第一个愿意使用的用户,第一个付费的用户,当时特别的兴奋,虽然在亏钱。。

软件使用的人慢慢多了之后,那么就跟之前不一样了,没人用的时候,软件挂了之后去重启就好了,产生不了任何价值,自然也就没有太多的压力。现在有不少用户了,而且是付费的用户,那么就需要对用户负责,系统肯定不能挂。我需要了解整个系统的状态。并且一旦系统出现异常时及时通知我进行恢复,这时候我开始考虑花心思去构建一个监控相关的基础服务,以达到以下目的:

  • 故障通知
  • 监控应用状态
  • 监控应用内部数据

对于实现这些目的,我的要求只有一个,那就是快速搞好,最好一天之内能够解决掉,因为这个不是我的重点,数据丢失无关紧要。

这里面比较关注的是故障通知,通知要比较及时,采用邮件的方式肯定是不行的,短信又太贵,最终我选择了一个比较罕见的方式,使用微信公众号,推送模版消息,这种方式能够很好的解决我的问题,一方面是免费的,另一方面一天有最多十万条,所以次数肯定是满足了的,有过公众号开发的可能知道模版推送需要资质,个人不能使用,正是因为个人使用,所以没有太深究,既然都不深究了,那么公众号直接拿测试公众号即可。

应用状态监控,应用内部数据监控这种有太多的解决方案了,我选择了一种比较熟悉的方式,监控显示用的是grafana,grafana数据源用的influxdb。把数据往influxdb里面插即可,influxdb提供了http接口,对大部分开发人员而言问题都不大。接下来就是将应用数据指标上报的influxdb当中,这里需要开发量,有两种方式:

  • 开发一个基础库,基础库嵌入到应用程序当中,应用程序直接写入数据到influxdb
  • 开发一个agent和一个基础库,基础库还是嵌入到应用程序当中,但是数据是上报到agent,由agent写到influxdb。

最后选择了第二种方式,第一种方式虽然只有一个基础库,但是后续如果我需要调整grafana的数据源,不用influxdb了,那所有应用都需要进行适配,采用第二种方式只有agent需要适配。而且评估了下,开发周期相差也不大。

最终整体的监控就采用这么个过程:

image

开发任务有两个,其一是开发agent,agent通过http调用influxdb,其二是开发api,让应用程序能够简单的使用,并且不能阻塞应用程序的运行,agent与api之间的通信采用的是grpc客户端流模式,整个思路应该算是很多公司都在采用的一种方式,只是个别地方可能不太一样,整体类似。

把上面的任务分解之后接下来就是一步一步把整个流程执行一遍,我分为以下几个步骤进行:

  • 先用安装好influxdb和grafana并做好配置
  • 开发agent程序和基础库,并提供接口给其他服务接入

完成以上两个个步骤之后,整个系统的监控基本上可以有效的运行了。

安装和配置influxdb,grafana

这个相对比较容易,到官网去下载运行就行。
grafana

influxdb

开发agent程序

agent就是个中间件,接收api发送过来的rpc请求,将rpc转换成http发送给influxdb。

首先是与api交互部分。
pb协议定义好接口以及接口参数,指明使用客户端流的方式,这样客户端就不需要每次发送数据都打开一条stream了。

syntax = "proto3";

package holenat.proto;

option go_package = "/proto/stat";

message SetTagReq {
  string prefix = 1;
  string tag = 2;
  int64 value = 3;
  string token = 4; // token验证
}

message SetTagReply {}

service StatService {
  rpc SetTag(stream SetTagReq) returns (SetTagReply) {}
}

agent的grpc server部分代码:


func (s *Server) SetTag(stream stat.StatService_SetTagServer) error {
	for {
		req, err := stream.Recv()
		if err != nil {
			return err
		}

		s.handleReq(req)
	}
}

func (s *Server) handleReq(req *stat.SetTagReq) {
	attrs := strings.Split(req.Tag, ".")
	s.db.SetTag(req.Prefix, attrs, req.Value)
}

func (db *InfluxDB) SetTag(prefix string, attrs []string, value int64) {
	str := prefix + "-"
	for i, attr := range attrs {
		if len(attr) > 0 {
			str = str + fmt.Sprintf(",t%d=%s", i, attr)
		}
	}
	str += fmt.Sprintf(" value=%d", value)

	br := bytes.NewBuffer([]byte(str))
	resp, err := http.Post(db.url, "application/x-www-form-urlencoded", br)
	if err != nil {
		log.Warn("post data fail: %v", err)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != 204 {
		log.Warn("set tag fail, status code %d", resp.StatusCode)
	}
}

因为有多个tag,可以grpc直接传tag的value数组,也可以传字符串,然后通过某种规则分割,这里通过.来进行了一次分割,field key硬编码为value,field value由接口传入。

server部分对应用是不可见的,对应用真正可见的是api层:

api层设计两个接口,一个是初始化的Init接口,另外一个是上报的接口

func Init(prefix, monitor string) {
	c, err := NewClient(&ClientConfig{
		Addr:   monitor,
		Prefix: prefix,
	})

	if err != nil {
		fmt.Println(err)
		return
	}

	cli = c
	go loop()
	go reportProfile()
}

func SetTag(tag string, value int) {
	req := &writeReq{
		tag:   tag,
		value: int64(value),
	}

	select {
	case channel <- req:
	default:
	}
}

在使用时,只需要在程序启动之初调用Init,后续程序内部数据需要监控调用SetTag,这让就可以让使用的人更加傻瓜的去用,不去深究内部细节了。

完成之后先放到一个无足轻重的应用去试一下,还不错,至少以后不用再去查数据库了。

image

最后说点现实点的问题

类似监控等应用场景,在服务器选择上,我原本考虑选择的是云服务器,但是算了下成本有点高,觉得不太值,然后看了下阿里云的轻量级服务器,在香港地区的收费还可以,一年两百八十左右,共享带宽,峰值30Mbps,每个月有1TB的流量,所以我就将上面应用部署在轻量级服务器上。

缺点是跟云服务器不同属一个VPC,所以安全性会有点问题,这个还需要自己通过iptables做白名单控制。这里不得不吐槽一下阿里云的轻量级云服务器的防火墙,这个防火墙配置页面做得很糙,没有ip配置,之后端口配置,连具体防火墙规则的备注都没有,果真是轻量级了。

Notr用户数破百

如果按照创建管理员用户作为上线时间的话,Notr内网穿透已经上线将近一年了,第一个用户是在2018年七月份,期间做过一次大的方案修改,最大的一次方案修改是春节放假期间把VPN的方式放弃,采用了新的思路去解决这个问题。终于软件整体趋于稳定,用户也很少报bug,也推出了license模式。近期由于推广力度加大,用户数量也在稳步上升,今天算是达到了三位数。在这一年期间碰到过很多非常友好也很有意思的用户,这将是Notr软件给我带来的另外的价值,这对目前对我而言比软件本身挣不挣钱更加重要,可以很肯定对说,软件肯定是不挣钱的。

在此记录下一些印象比较深刻的用户。

第一个用户

首个用户是2018年七月注册的,当时是在一个群里面试着告诉大家我在开发一个软件,看谁有兴趣用,然后当时就有个群友联系了我,说想用下,我说windows要安装虚拟网卡,装完然后就用,对方想用来穿透windows远程桌面服务的,我说你可以穿透tcp 3389端口就行了。然后第一次用户使用以失败告终,主要是没考虑windows防火墙的问题,然后代码里面手动把VPN的地址段都放行,最后才勉强可以。

来自粉丝圈的用户

我是很喜欢杨超越的,主要是太搞笑了,然后之前杨超越粉丝圈组织了一个超越杯编程大赛,当时就想,哎,像这种远程协作的,要调试接口的时候就用得到啊,如果做小程序或者微信公众号的时候,也用得到啊。然后就在微博和知乎发贴,当时说的是免费提供服务,直到比赛结束。谁知道编程大赛没给我带来多少流量,但是有个哥们却跟我说想一起做,然后想了下觉得可以,有兴趣就行,然后就开始建了github私有项目。不过最终这个哥们还是没有一起做下去,主要是时间问题还有本身也不太了解这方面知识。

爽快又不爽快的用户

前两天跟朋友吃饭,说起了最近有个用户买了一年license,很爽快,然后聊着聊着就回想这个用户注册的用户名是什么,然后想起来对方居然是当初我看着用了一个月的tcp服务,试用期过了就只用http,而且用的还是旧版本,当时就觉得,我去,本来就有需求的,一个月花点钱买个又怎样呢。当时没有验证注册用户的邮箱,对方填了个假邮箱,我联系不上,最后我换了个新版本,我把老版本一刀切,所有老版本的程序都在后台返回了版本过旧,目的是希望对方能够进入官网加群。后来对方确实加了群,在license功能推出来之后,立马买了一年的license。。

很难搞的用户

没有企业资质在**做不了支付,然后我就在软件登录之后,链接到淘宝商品,让对方去淘宝下单,但是也有用户是在淘宝搜一个一个翻找到我挂在淘宝上的软件的。其中有一个印象最深,对方找了我两次,前后相隔一个月,第一次的时候对方问我限速多少,我说3m,对方说是300多k的那种吗,我说3mbps,然后对方立马说再见,过一会又回来说,我的是ubuntu喔,可以吗,我说可以,校内网喔,可以上网吗?可以,那就没问题。校内网。。我知道校内网。。然后就没有然后了。再过将近一个月,对方又通过淘宝找到了我,问我限速多少,我说3mbps,行吧。然后叫我教他怎么用,我说注册,下载,运行。你到第三步的时候跟我说我告诉你有哪些配置,然后对方不注册,直接下载运行,跑了三个小时,我说你可以注册,注册成功之后会给你一个固定的域名。然后对方又不了了之了。

海外用户

这类用户我也觉得很神奇,我从来没在海外的站点发布过。对方说是在知乎看到我的回答然后找到我的,之前用的ngrok太卡了,完全不能用,就想试试看能不能行。当时香港的云服务器已经关闭了,对方用我的客户端也会被调度到深圳的节点,对方不是很满意,延时两百多毫秒,有点卡,我说你等下,我开个香港的节点给你用下试试,然后延时瞬间降了两百毫秒,跟本地使用ssh一样。但是还不行,我说你现在之所以能够连上香港的,是因为我现在调度策略很简单,就根据客户端数量均衡,香港节点现在刚启动,所以很自然就选到了香港。我这边需要加上根据客户端ip进行调度的功能,然后第二天上线之后对方觉得可以接受。

总的来说,我对这个软件根本不报任何挣钱的期望,开发这个也不是为了挣钱,当用户量较小的时候,本来成本就不高,当用户量大的时候,收取的软件费用足以支撑服务器成本,当时通过这个软件能够让我有一个服务意识,然后也接触了不少用户。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.