diff --git a/.gitignore b/.gitignore
index 66fd13c..e26bd7b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,8 @@
*.out
# Dependency directories (remove the comment below to include it)
-# vendor/
+vendor/
+
+
+.idea/
+anylink
\ No newline at end of file
diff --git a/README.md b/README.md
index 5b96c1d..6864664 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,71 @@
-# anylink
-AnyLink是一个企业级远程办公vpn软件
+# AnyLink
+
+AnyLink 是一个企业级远程办公vpn软件,可以支持多人同时在线使用。
+
+## Introduction
+
+AnyLink 基于 [ietf-openconnect](https://tools.ietf.org/html/draft-mavrogiannopoulos-openconnect-02) 协议开发,并且借鉴了 [ocserv](http://ocserv.gitlab.io/www/index.html) 的开发思路,使其可以同时兼容 AnyConnect 客户端。
+
+AnyLink 使用TLS/DTLS进行数据加密,因此需要RSA或ECC证书,可以通过 Let's Encrypt 和 TrustAsia 申请免费的SSL证书。
+
+AnyLink 服务端仅在CentOs7测试通过,如需要安装在其他系统,需要服务端支持tun功能、ip设置命令。
+
+## Installation
+
+```
+git clone https://github.com/bjdgyc/anylink.git
+cd anylink
+go build -o anylink -ldflags "-X main.COMMIT_ID=`git rev-parse HEAD`"
+#注意使用root权限运行
+sudo ./anylink -conf="conf/server.toml"
+```
+
+## Feature
+
+- [x] IP分配
+- [x] TLS-TCP通道
+- [x] 兼容AnyConnect
+- [x] 多用户支持
+- [ ] DTLS-UDP通道
+- [ ] 后台管理界面
+- [ ] 用户组支持
+- [ ] TOTP令牌支持
+- [ ] 流量控制
+- [ ] 访问权限管理
+
+## Config
+
+- [conf/server.toml](https://github.com/bjdgyc/anylink/blob/master/conf/server.toml)
+- [conf/user.toml](https://github.com/bjdgyc/anylink/blob/master/conf/user.toml)
+
+## Setting
+
+1. 开启服务器转发
+ ```
+ # flie: /etc/sysctl.conf
+ net.ipv4.ip_forward = 1
+
+ #执行如下命令
+ sysctl -w net.ipv4.ip_forward=1
+ ```
+
+2. 设置nat转发规则
+ ```
+ # eth0为服务器内网网卡
+ iptables -t nat -A POSTROUTING -s 192.168.10.0/255.255.255.0 -o eth0 -j MASQUERADE
+ ```
+
+3. 使用AnyConnect客户端连接即可
+
+
+## License
+
+本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 LICENSE 文件中。
+
+
+
+
+
+
+
+
diff --git a/common/app_ver.go b/common/app_ver.go
new file mode 100644
index 0000000..485658c
--- /dev/null
+++ b/common/app_ver.go
@@ -0,0 +1,6 @@
+package common
+
+const (
+ APP_NAME = "AnyLink"
+ APP_VER = "0.0.1"
+)
diff --git a/common/assert_test.go b/common/assert_test.go
new file mode 100644
index 0000000..90fe9c2
--- /dev/null
+++ b/common/assert_test.go
@@ -0,0 +1,10 @@
+package common
+
+import "testing"
+
+func AssertTrue(t *testing.T, a bool) {
+ t.Helper()
+ if !a {
+ t.Errorf("Not True %t", a)
+ }
+}
diff --git a/common/cfg_server.go b/common/cfg_server.go
new file mode 100644
index 0000000..a302ad2
--- /dev/null
+++ b/common/cfg_server.go
@@ -0,0 +1,77 @@
+package common
+
+import (
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+
+ "github.com/pelletier/go-toml"
+)
+
+var (
+ ServerCfg = &ServerConfig{}
+)
+
+// # ReKey time (in seconds)
+// rekey-time = 172800
+// # ReKey method
+// # Valid options: ssl, new-tunnel
+// # ssl: Will perform an efficient rehandshake on the channel allowing
+// # a seamless connection during rekey.
+// # new-tunnel: Will instruct the client to discard and re-establish the channel.
+// # Use this option only if the connecting clients have issues with the ssl
+// # option.
+// rekey-method = ssl
+
+type ServerConfig struct {
+ UserFile string `toml:"user_file"`
+ ServerAddr string `toml:"server_addr"`
+ DebugAddr string `toml:"debug_addr"`
+ CertFile string `toml:"cert_file"`
+ CertKey string `toml:"cert_key"`
+ LinkGroups []string `toml:"link_groups"`
+ DefaultGroup string `toml:"default_group"`
+ Banner string `toml:"banner"` // 欢迎语
+ CstpDpd int `toml:"cstp_dpd"` // Dead peer detection in seconds
+ CstpKeepalive int `toml:"cstp_keepalive"` // in seconds
+ SessionTimeout int `toml:"session_timeout"` // in seconds
+ AuthTimeout int `toml:"auth_timeout"` // in seconds
+ MaxClient int `toml:"max_client"`
+ MaxUserClient int `toml:"max_user_client"`
+ Ipv4Network string `toml:"ipv4_network"` // 192.168.1.0
+ Ipv4Netmask string `toml:"ipv4_netmask"` // 255.255.255.0
+ Ipv4GateWay string `toml:"-"`
+ Include []string `toml:"include"` // 10.10.10.0/255.255.255.0
+ Exclude []string `toml:"exclude"` // 192.168.5.0/255.255.255.0
+ ClientDns []string `toml:"client_dns"` // 114.114.114.114
+ AllowLan bool `toml:"allow_lan"` // 允许本地LAN访问vpn网络
+}
+
+func loadServer() {
+ b, err := ioutil.ReadFile(serverFile)
+ if err != nil {
+ panic(err)
+ }
+ err = toml.Unmarshal(b, ServerCfg)
+ if err != nil {
+ panic(err)
+ }
+
+ sf, _ := filepath.Abs(serverFile)
+ base := filepath.Dir(sf)
+
+ // 转换成绝对路径
+ ServerCfg.UserFile = getAbsPath(base, ServerCfg.UserFile)
+ ServerCfg.CertFile = getAbsPath(base, ServerCfg.CertFile)
+ ServerCfg.CertKey = getAbsPath(base, ServerCfg.CertKey)
+
+ fmt.Printf("ServerCfg: %+v \n", ServerCfg)
+}
+
+func getAbsPath(base, cfile string) string {
+ abs := filepath.IsAbs(cfile)
+ if abs {
+ return cfile
+ }
+ return filepath.Join(base, cfile)
+}
diff --git a/common/cfg_user.go b/common/cfg_user.go
new file mode 100644
index 0000000..f023dc6
--- /dev/null
+++ b/common/cfg_user.go
@@ -0,0 +1,91 @@
+package common
+
+import (
+ "crypto/sha1"
+ "fmt"
+ "io/ioutil"
+ "sync"
+
+ "github.com/pelletier/go-toml"
+)
+
+var (
+ users = map[string]User{}
+ limitClient = map[string]int{"_all": 0}
+ limitMux = sync.Mutex{}
+)
+
+type User struct {
+ Group string `toml:"group"`
+ Username string `toml:"-"`
+ Password string `toml:"password"`
+ OtpSecret string `toml:"otp_secret"`
+}
+
+func CheckUser(name, pwd, group string) bool {
+ user, ok := users[name]
+ if !ok {
+ return false
+ }
+ pwdHash := hashPass(pwd)
+ if user.Password == pwdHash {
+ return true
+ }
+ return false
+}
+
+func hashPass(pwd string) string {
+ sum := sha1.Sum([]byte(pwd))
+ return fmt.Sprintf("%x", sum)
+}
+
+func LimitClient(name string, close bool) bool {
+ limitMux.Lock()
+ defer limitMux.Unlock()
+ // defer fmt.Println(limitClient)
+
+ _all := limitClient["_all"]
+ c, ok := limitClient[name]
+ if !ok { // 不存在用户
+ limitClient[name] = 0
+ }
+
+ if close {
+ limitClient[name] = c - 1
+ limitClient["_all"] = _all - 1
+ return true
+ }
+
+ // 全局判断
+ if _all >= ServerCfg.MaxClient {
+ return false
+ }
+
+ // 超出同一个用户限制
+ if c >= ServerCfg.MaxUserClient {
+ return false
+ }
+
+ limitClient[name] = c + 1
+ limitClient["_all"] = _all + 1
+ return true
+}
+
+func loadUser() {
+ b, err := ioutil.ReadFile(ServerCfg.UserFile)
+ if err != nil {
+ panic(err)
+ }
+ err = toml.Unmarshal(b, &users)
+ if err != nil {
+ panic(err)
+ }
+
+ // 添加用户名
+ for k, v := range users {
+ v.Username = k
+ users[k] = v
+ }
+
+ fmt.Println("users:", users)
+}
diff --git a/common/cfg_user_test.go b/common/cfg_user_test.go
new file mode 100644
index 0000000..6466534
--- /dev/null
+++ b/common/cfg_user_test.go
@@ -0,0 +1,33 @@
+package common
+
+import (
+ "testing"
+)
+
+func TestCheckUser(t *testing.T) {
+ users["user1"] = User{Password: "7c4a8d09ca3762af61e59520943dc26494f8941b"}
+ users["user2"] = User{Password: "7c4a8d09ca3762af61e59520943dc26494f8941c"}
+
+ var res bool
+ res = CheckUser("user1", "123456", "")
+ AssertTrue(t, res == true)
+
+ res = CheckUser("user2", "123457", "")
+ AssertTrue(t, res == false)
+}
+
+func TestLimitClient(t *testing.T) {
+ ServerCfg.MaxClient = 2
+ ServerCfg.MaxUserClient = 1
+
+ res1 := LimitClient("user1", false)
+ res2 := LimitClient("user1", false)
+ res3 := LimitClient("user2", false)
+ res4 := LimitClient("user3", false)
+
+ AssertTrue(t, res1 == true)
+ AssertTrue(t, res2 == false)
+ AssertTrue(t, res3 == true)
+ AssertTrue(t, res4 == false)
+
+}
diff --git a/common/flag.go b/common/flag.go
new file mode 100644
index 0000000..bf0c437
--- /dev/null
+++ b/common/flag.go
@@ -0,0 +1,44 @@
+package common
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "runtime"
+)
+
+var (
+ // 提交id
+ CommitId string
+ // 配置文件
+ serverFile string
+ passwd string
+ // 显示版本信息
+ rev bool
+)
+
+func initFlag() {
+ flag.StringVar(&serverFile, "conf", "./conf/server.toml", "server config file path")
+ flag.StringVar(&passwd, "pass", "", "generation a sha1 password")
+ flag.BoolVar(&rev, "rev", false, "display version info")
+ flag.Parse()
+
+ if passwd != "" {
+ pwdHash := hashPass(passwd)
+ fmt.Printf("passwd-sha1:%s\n", pwdHash)
+ os.Exit(0)
+ }
+
+ if rev {
+ fmt.Printf("%s v%s build on %s [%s, %s] commit_id(%s) \n",
+ APP_NAME, APP_VER, runtime.Version(), runtime.GOOS, runtime.GOARCH, CommitId)
+ os.Exit(0)
+ }
+}
+
+func InitConfig() {
+ initFlag()
+ loadServer()
+ loadUser()
+ initIpPool()
+}
diff --git a/common/ip_pool.go b/common/ip_pool.go
new file mode 100644
index 0000000..3f84e20
--- /dev/null
+++ b/common/ip_pool.go
@@ -0,0 +1,139 @@
+package common
+
+import (
+ "encoding/binary"
+ "math"
+ "net"
+ "sync"
+ "time"
+)
+
+const (
+ // ip租期 (秒)
+ IpLease = 1209600
+)
+
+var (
+ ipPool = &IpPoolConfig{}
+ macIps = map[string]*MacIp{}
+)
+
+type MacIp struct {
+ IsActive bool
+ Ip net.IP
+ MacAddr string
+ LastLogin time.Time
+}
+
+type IpPoolConfig struct {
+ mux sync.Mutex
+ // 计算动态ip
+ Ipv4Net *net.IPNet
+ Ipv4GateWay net.IP
+ IpLongMin uint32
+ IpLongMax uint32
+ IpLongNow uint32
+}
+
+func initIpPool() {
+ // ip地址
+ ip := net.ParseIP(ServerCfg.Ipv4Network)
+ // 子网掩码
+ maskIp := net.ParseIP(ServerCfg.Ipv4Netmask).To4()
+ mask := net.IPMask(maskIp)
+
+ ipNet := &net.IPNet{IP: ip, Mask: mask}
+ ipPool.Ipv4Net = ipNet
+
+ // 网络地址零值
+ min := binary.BigEndian.Uint32(ip.Mask(mask))
+ // 广播地址
+ one, _ := ipNet.Mask.Size()
+ max := min | uint32(math.Pow(2, float64(32-one))-1)
+
+ min += 1 // 网关
+ ipPool.Ipv4GateWay = long2ip(min)
+ ServerCfg.Ipv4GateWay = ipPool.Ipv4GateWay.String()
+ // 第一个可用地址
+ min += 1
+ ipPool.IpLongMin = min
+ ipPool.IpLongMax = max
+ ipPool.IpLongNow = min
+}
+
+func long2ip(i uint32) net.IP {
+ ip := make([]byte, 4)
+ binary.BigEndian.PutUint32(ip, i)
+ return ip
+}
+
+// 获取动态ip
+func AcquireIp(macAddr string) net.IP {
+ ipPool.mux.Lock()
+ defer ipPool.mux.Unlock()
+ tNow := time.Now()
+
+ // 判断已经分配过
+ if mi, ok := macIps[macAddr]; ok {
+ mi.IsActive = true
+ mi.LastLogin = tNow
+ return mi.Ip
+ }
+
+ // ip池分配完之前
+ if ipPool.IpLongNow < ipPool.IpLongMax {
+ // 递增分配一个ip
+ ip := long2ip(ipPool.IpLongNow)
+ mi := &MacIp{IsActive: true, Ip: ip, MacAddr: macAddr, LastLogin: tNow}
+ macIps[macAddr] = mi
+ ipPool.IpLongNow += 1
+ return ip
+ }
+
+ // 查找过期数据
+ farMi := &MacIp{LastLogin: tNow}
+ for k, v := range macIps {
+ // 跳过活跃连接
+ if v.IsActive {
+ continue
+ }
+
+ // 已经超过租期
+ if tNow.Sub(v.LastLogin) > IpLease*time.Second {
+ delete(macIps, k)
+ ip := v.Ip
+ mi := &MacIp{IsActive: true, Ip: ip, MacAddr: macAddr, LastLogin: tNow}
+ macIps[macAddr] = mi
+ return ip
+ }
+
+ // 其他情况判断最早登陆的mac
+ if v.LastLogin.Before(farMi.LastLogin) {
+ farMi = v
+ }
+ }
+
+ // 全都在线,没有数据可用
+ if farMi.MacAddr == "" {
+ return nil
+ }
+
+ // 使用最早登陆的mac地址
+ delete(macIps, farMi.MacAddr)
+ ip := farMi.Ip
+ mi := &MacIp{IsActive: true, Ip: ip, MacAddr: macAddr, LastLogin: tNow}
+ macIps[macAddr] = mi
+ return ip
+}
+
+// 回收ip
+func ReleaseIp(ip net.IP, macAddr string) {
+ ipPool.mux.Lock()
+ defer ipPool.mux.Unlock()
+ if mi, ok := macIps[macAddr]; ok {
+ if mi.Ip.Equal(ip) {
+ mi.IsActive = false
+ mi.LastLogin = time.Now()
+ }
+ }
+}
diff --git a/common/ip_pool_test.go b/common/ip_pool_test.go
new file mode 100644
index 0000000..94ba1ec
--- /dev/null
+++ b/common/ip_pool_test.go
@@ -0,0 +1,50 @@
+package common
+
+import (
+ "fmt"
+ "net"
+ "testing"
+)
+
+func TestAcquireIp(t *testing.T) {
+ ServerCfg.Ipv4Network = "192.168.1.0"
+ ServerCfg.Ipv4Netmask = "255.255.255.0"
+ macIps = map[string]*MacIp{}
+ initIpPool()
+
+ var ip net.IP
+
+ for i := 2; i <= 100; i++ {
+ ip = AcquireIp(fmt.Sprintf("mac-%d", i))
+ }
+ ip = AcquireIp(fmt.Sprintf("mac-new"))
+ AssertTrue(t, ip.Equal(net.IPv4(192, 168, 1, 101)))
+ for i := 102; i <= 254; i++ {
+ ip = AcquireIp(fmt.Sprintf("mac-%d", i))
+ }
+ ip = AcquireIp(fmt.Sprintf("mac-nil"))
+ AssertTrue(t, ip == nil)
+}
+
+func TestReleaseIp(t *testing.T) {
+ ServerCfg.Ipv4Network = "192.168.1.0"
+ ServerCfg.Ipv4Netmask = "255.255.255.0"
+ macIps = map[string]*MacIp{}
+ initIpPool()
+
+ var ip net.IP
+
+ // 分配完所有数据
+ for i := 2; i <= 254; i++ {
+ ip = AcquireIp(fmt.Sprintf("mac-%d", i))
+ }
+
+ ip = AcquireIp(fmt.Sprintf("mac-more"))
+ AssertTrue(t, ip == nil)
+
+ ReleaseIp(net.IPv4(192, 168, 1, 123), "mac-123")
+ ReleaseIp(net.IPv4(192, 168, 1, 100), "mac-100")
+ ip = AcquireIp(fmt.Sprintf("mac-new"))
+ // 最早过期的ip
+ AssertTrue(t, ip.Equal(net.IPv4(192, 168, 1, 123)))
+}
diff --git a/common/log.go b/common/log.go
new file mode 100644
index 0000000..18b8cb0
--- /dev/null
+++ b/common/log.go
@@ -0,0 +1,7 @@
+package common
+
+import "log"
+
+func init() {
+ log.SetFlags(log.LstdFlags | log.Lshortfile)
+}
diff --git a/conf/.gitignore b/conf/.gitignore
new file mode 100644
index 0000000..eef0a03
--- /dev/null
+++ b/conf/.gitignore
@@ -0,0 +1,3 @@
+#过滤本地证书文件
+vpn_cert.key
+vpn_cert.pem
\ No newline at end of file
diff --git a/conf/server.toml b/conf/server.toml
new file mode 100644
index 0000000..0d765b8
--- /dev/null
+++ b/conf/server.toml
@@ -0,0 +1,48 @@
+#服务配置信息
+
+#其他配置文件,可以使用绝对路径
+#或者相对于server.toml的路径
+user_file = "./user.toml"
+#证书文件
+cert_file = "./vpn_cert.pem"
+cert_key = "./vpn_cert.key"
+
+#服务监听的地址
+server_addr = ":443"
+debug_addr = "127.0.0.1:8800"
+
+#用户组
+link_groups = ["one", "two"]
+#默认选择的组
+default_group = "one"
+
+#登陆成功的欢迎语
+banner = "您已接入公司网络,请按照公司规定使用。\n请勿进行非工作下载及视频行为!"
+
+#客户端失效检测时间(秒) dpd > keepalive
+cstp_dpd = 30
+cstp_keepalive = 20
+#session过期时间,用于断线重连,0永不过期
+session_timeout = 3600
+auth_timeout = 0
+
+
+#最大客户端数量
+max_client = 300
+#单个用户同时在线数量
+max_user_client = 3
+
+#客户端分配的ip地址池
+ipv4_network = "192.168.10.0"
+ipv4_netmask = "255.255.255.0"
+#需加密传输的ip规则
+#include = ["10.10.10.0/255.255.255.0"]
+#非加密传输的ip规则
+#exclude = ["192.168.5.0/255.255.255.0"]
+#客户端使用的dns
+client_dns = ["114.114.114.114"]
+#是否允许本地LAN访问vpn网络
+allow_lan = true
+
+
+
diff --git a/conf/user.toml b/conf/user.toml
new file mode 100644
index 0000000..eb4aa57
--- /dev/null
+++ b/conf/user.toml
@@ -0,0 +1,17 @@
+#用户信息配置
+[test]
+group = "group1"
+#密码需要使用 sha1,以下密码为 123456
+password = "7c4a8d09ca3762af61e59520943dc26494f8941b"
+
+
+[user]
+group = "group2"
+#以下密码为 123456
+password = "7c4a8d09ca3762af61e59520943dc26494f8941b"
+
+
+
+
+
+
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..bb165b6
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+module github.com/bjdgyc/anylink
+
+go 1.14
+
+require (
+ github.com/julienschmidt/httprouter v1.3.0
+ github.com/pelletier/go-toml v1.8.0
+ github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
+ golang.org/x/sys v0.0.0-20200817155316-9781c653f443 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..605c35a
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,16 @@
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
+github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
+github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
+github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
+golang.org/x/sys v0.0.0-20200817155316-9781c653f443 h1:X18bCaipMcoJGm27Nv7zr4XYPKGUy92GtqboKC2Hxaw=
+golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/handler/base.go b/handler/base.go
new file mode 100644
index 0000000..4dcf5ba
--- /dev/null
+++ b/handler/base.go
@@ -0,0 +1,74 @@
+package handler
+
+import (
+ "encoding/xml"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/julienschmidt/httprouter"
+)
+
+type ClientRequest struct {
+ XMLName xml.Name `xml:"config-auth"`
+ Client string `xml:"client,attr"` // 一般都是 vpn
+ Type string `xml:"type,attr"` // 请求类型 init logout auth-reply
+ AggregateAuthVersion string `xml:"aggregate-auth-version,attr"` // 一般都是 2
+ Version string `xml:"version"` // 客户端版本号
+ GroupAccess string `xml:"group-access"` // 请求的地址
+ GroupSelect string `xml:"group-select"` // 选择的组名
+ SessionId string `xml:"session-id"`
+ SessionToken string `xml:"session-token"`
+ Auth auth `xml:"auth"`
+ DeviceId deviceId `xml:"device-id"`
+ MacAddressList macAddressList `xml:"mac-address-list"`
+}
+
+type auth struct {
+ Username string `xml:"username"`
+ Password string `xml:"password"`
+}
+
+type deviceId struct {
+ ComputerName string `xml:"computer-name,attr"`
+ DeviceType string `xml:"device-type,attr"`
+ PlatformVersion string `xml:"platform-version,attr"`
+ UniqueId string `xml:"unique-id,attr"`
+ UniqueIdGlobal string `xml:"unique-id-global,attr"`
+}
+
+type macAddressList struct {
+ MacAddress string `xml:"mac-address"`
+}
+
+// 判断anyconnect客户端
+func checkVpnClient(h httprouter.Handle) httprouter.Handle {
+ return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
+ // TODO 调试信息输出
+ // hd, _ := httputil.DumpRequest(r, true)
+ // fmt.Println("DumpRequest: ", string(hd))
+
+ user_Agent := strings.ToLower(r.UserAgent())
+ x_Aggregate_Auth := r.Header.Get("X-Aggregate-Auth")
+ x_Transcend_Version := r.Header.Get("X-Transcend-Version")
+ if strings.Contains(user_Agent, "anyconnect") &&
+ x_Aggregate_Auth == "1" && x_Transcend_Version == "1" {
+ h(w, r, ps)
+ } else {
+ w.WriteHeader(http.StatusForbidden)
+ fmt.Fprintf(w, "error request")
+ }
+ }
+}
+
+func setCommonHeader(w http.ResponseWriter) {
+ // Content-Length Date 默认已经存在
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.Header().Set("Cache-Control", "no-store")
+ w.Header().Set("Pragma", "no-cache")
+ w.Header().Set("Transfer-Encoding", "chunked")
+ w.Header().Set("Connection", "keep-alive")
+ w.Header().Set("X-Frame-Options", "SAMEORIGIN")
+ w.Header().Set("X-Aggregate-Auth", "1")
+ w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
+}
diff --git a/handler/dtls.go b/handler/dtls.go
new file mode 100644
index 0000000..32c9470
--- /dev/null
+++ b/handler/dtls.go
@@ -0,0 +1,6 @@
+package handler
+
+// 暂时没有实现
+func startDtls() {
+
+}
diff --git a/handler/link_auth.go b/handler/link_auth.go
new file mode 100644
index 0000000..af79224
--- /dev/null
+++ b/handler/link_auth.go
@@ -0,0 +1,151 @@
+package handler
+
+import (
+ "encoding/xml"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "strings"
+ "text/template"
+
+ "github.com/bjdgyc/anylink/common"
+ "github.com/julienschmidt/httprouter"
+)
+
+func LinkAuth(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
+ body, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ defer r.Body.Close()
+
+ cr := ClientRequest{}
+ err = xml.Unmarshal(body, &cr)
+ if err != nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ // fmt.Printf("%+v \n", cr)
+
+ setCommonHeader(w)
+ if cr.Type == "logout" {
+ // 退出删除session信息
+ if cr.SessionToken != "" {
+ DelSessByStoken(cr.SessionToken)
+ }
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ if cr.Type == "init" {
+ w.WriteHeader(http.StatusOK)
+ data := RequestData{Group: cr.GroupSelect, Groups: common.ServerCfg.LinkGroups}
+ tplRequest(tpl_request, w, data)
+ return
+ }
+
+ // 登陆参数判断
+ if cr.Type != "auth-reply" || cr.Auth.Username == "" || cr.Auth.Password == "" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ // TODO 用户密码校验
+ if !common.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect) {
+ w.WriteHeader(http.StatusOK)
+ data := RequestData{Group: cr.GroupSelect, Groups: common.ServerCfg.LinkGroups, Error: true}
+ tplRequest(tpl_request, w, data)
+ return
+ }
+
+ // 创建新的session信息
+ sess := NewSession()
+ sess.UserName = cr.Auth.Username
+ sess.MacAddr = strings.ToLower(cr.MacAddressList.MacAddress)
+ cd := RequestData{SessionId: sess.Sid, SessionToken: sess.Sid + "@" + sess.Token,
+ Banner: common.ServerCfg.Banner}
+ w.WriteHeader(http.StatusOK)
+ tplRequest(tpl_complete, w, cd)
+}
+
+const (
+ tpl_request = iota
+ tpl_complete
+)
+
+func tplRequest(typ int, w io.Writer, data RequestData) {
+ if typ == tpl_request {
+ t, _ := template.New("auth_request").Parse(auth_request)
+ t.Execute(w, data)
+ return
+ }
+
+ if strings.Contains(data.Banner, "\n") {
+ // 替换xml文件的换行符
+ data.Banner = strings.ReplaceAll(data.Banner, "\n", "
")
+ }
+ t, _ := template.New("auth_complete").Parse(auth_complete)
+ t.Execute(w, data)
+}
+
+// 设置输出信息
+type RequestData struct {
+ Groups []string
+ Group string
+ Error bool
+ // complete
+ SessionId string
+ SessionToken string
+ Banner string
+}
+
+var auth_request = `
+
+
+ {{.Group}}
+ {{.Group}}
+ 168179266
+ 1595829378234
+ multiple-cert
+ single-sign-on-v2
+
+
+ Login
+ 请输入你的用户名和密码
+
+ {{if .Error}}
+ 登陆失败: %s
+ {{end}}
+
+
+
+`
+
+var auth_complete = `
+
+ {{.SessionId}}
+ {{.SessionToken}}
+
+ {{.Banner}}
+
+
+
+ ssl-dhe
+
+
+
+ 240B97A685B2BFA66AD699B90AAC49EA66495D69
+
+
+
+
+`
diff --git a/handler/link_cstp.go b/handler/link_cstp.go
new file mode 100644
index 0000000..4d8337e
--- /dev/null
+++ b/handler/link_cstp.go
@@ -0,0 +1,107 @@
+package handler
+
+import (
+ "encoding/binary"
+ "log"
+ "net"
+ "time"
+
+ "github.com/bjdgyc/anylink/common"
+)
+
+func LinkCstp(conn net.Conn, sess *Session) {
+ // fmt.Println("HandlerCstp")
+ defer func() {
+ conn.Close()
+ sess.Close()
+ log.Println("LinkCstp return")
+ }()
+
+ var (
+ err error
+ dataLen uint16
+ dead = time.Duration(common.ServerCfg.CstpDpd+2) * time.Second
+ )
+
+ go cstpWrite(conn, sess)
+
+ for {
+ // 设置超时限制
+ err = conn.SetDeadline(time.Now().Add(dead))
+ if err != nil {
+ log.Println("SetDeadline: ", err)
+ return
+ }
+ hdata := make([]byte, 1500)
+ _, err = conn.Read(hdata)
+ if err != nil {
+ log.Println("read hdata: ", err)
+ return
+ }
+
+ switch hdata[6] {
+ case 0x07: // KEEPALIVE
+ // do nothing
+ // fmt.Println("keepalive")
+ case 0x05: // DISCONNECT
+ // fmt.Println("DISCONNECT")
+ return
+ case 0x03: // DPD-REQ
+ // fmt.Println("DPD-REQ")
+ payload := &Payload{
+ ptype: 0x04, // DPD-RESP
+ }
+ // 直接返回给客户端 resp
+ select {
+ case sess.PayloadOut <- payload:
+ case <-sess.Closed:
+ return
+ }
+ break
+ case 0x00:
+ dataLen = binary.BigEndian.Uint16(hdata[4:6]) // 4,5
+ payload := &Payload{
+ ptype: 0x00, // DPD-RESP
+ data: hdata[8 : 8+dataLen],
+ }
+ select {
+ case sess.PayloadIn <- payload:
+ case <-sess.Closed:
+ return
+ }
+ }
+ }
+}
+
+func cstpWrite(conn net.Conn, sess *Session) {
+ defer func() {
+ conn.Close()
+ sess.Close()
+ log.Println("cstpWrite return")
+ }()
+
+ var (
+ err error
+ header []byte
+ payload *Payload
+ )
+
+ for {
+ select {
+ case payload = <-sess.PayloadOut:
+ case <-sess.Closed:
+ return
+ }
+
+ header = []byte{'S', 'T', 'F', 0x01, 0x00, 0x00, payload.ptype, 0x00}
+ if payload.ptype == 0x00 { // data
+ binary.BigEndian.PutUint16(header[4:6], uint16(len(payload.data)))
+ header = append(header, payload.data...)
+ }
+ _, err = conn.Write(header)
+ if err != nil {
+ log.Println("write err", err)
+ return
+ }
+ }
+}
diff --git a/handler/link_home.go b/handler/link_home.go
new file mode 100644
index 0000000..41aaa7c
--- /dev/null
+++ b/handler/link_home.go
@@ -0,0 +1,26 @@
+package handler
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httputil"
+ "strings"
+
+ "github.com/julienschmidt/httprouter"
+)
+
+func LinkHome(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
+ hu, _ := httputil.DumpRequest(r, true)
+ fmt.Println("DumpHome: ", string(hu))
+ fmt.Println(r.RemoteAddr)
+
+ connection := strings.ToLower(r.Header.Get("Connection"))
+ if connection == "close" {
+ w.Header().Set("Connection", "close")
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintln(w, "hello world")
+}
diff --git a/handler/link_tun.go b/handler/link_tun.go
new file mode 100644
index 0000000..567a32d
--- /dev/null
+++ b/handler/link_tun.go
@@ -0,0 +1,124 @@
+package handler
+
+import (
+ "fmt"
+ "log"
+ "os/exec"
+
+ "github.com/bjdgyc/anylink/common"
+ "github.com/songgao/water"
+)
+
+func testTun() {
+ // 测试tun
+ cfg := water.Config{
+ DeviceType: water.TUN,
+ }
+
+ ifce, err := water.New(cfg)
+ if err != nil {
+ log.Fatal("open tun err: ", err)
+ }
+ // 测试ip命令
+ cmdstr := fmt.Sprintf("ip link set dev %s up mtu %s multicast off", ifce.Name(), "1399")
+ err = execCmd([]string{cmdstr})
+ if err != nil {
+ log.Fatal("ip cmd err: ", err)
+ }
+ ifce.Close()
+}
+
+// 创建tun网卡
+func LinkTun(sess *Session) {
+ defer func() {
+ sess.Close()
+ log.Println("LinkTun return")
+ }()
+
+ cfg := water.Config{
+ DeviceType: water.TUN,
+ }
+
+ ifce, err := water.New(cfg)
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ // log.Printf("Interface Name: %s\n", ifce.Name())
+ sess.TunName = ifce.Name()
+ defer ifce.Close()
+
+ // arp on
+ cmdstr1 := fmt.Sprintf("ip link set dev %s up mtu %s multicast off", ifce.Name(), sess.Mtu)
+ cmdstr2 := fmt.Sprintf("ip addr add dev %s local %s peer %s/32",
+ ifce.Name(), common.ServerCfg.Ipv4GateWay, sess.NetIp)
+ cmdstr3 := fmt.Sprintf("sysctl -w net.ipv6.conf.%s.disable_ipv6=1", ifce.Name())
+ cmdStrs := []string{cmdstr1, cmdstr2, cmdstr3}
+ err = execCmd(cmdStrs)
+ if err != nil {
+ return
+ }
+
+ go tunRead(ifce, sess)
+
+ var payload *Payload
+
+ for {
+ select {
+ case payload = <-sess.PayloadIn:
+ case <-sess.Closed:
+ return
+ }
+
+ // ip_src := waterutil.IPv4Source(payload.data)
+ // ip_des := waterutil.IPv4Destination(payload.data)
+ // ip_port := waterutil.IPv4DestinationPort(payload.data)
+ // fmt.Println("write: ", ip_src, ip_des.String(), ip_port, len(payload.data))
+
+ _, err = ifce.Write(payload.data)
+ if err != nil {
+ log.Println("tun Write err", err)
+ return
+ }
+ }
+
+}
+
+func tunRead(ifce *water.Interface, sess *Session) {
+ var (
+ err error
+ n int
+ )
+
+ for {
+ packet := make([]byte, 1500)
+ n, err = ifce.Read(packet)
+ if err != nil {
+ log.Println("tun Read err", n, err)
+ return
+ }
+
+ payload := &Payload{
+ ptype: 0x00,
+ data: packet[:n],
+ }
+
+ select {
+ case sess.PayloadOut <- payload:
+ case <-sess.Closed:
+ return
+ }
+ }
+}
+
+func execCmd(cmdStrs []string) error {
+ for _, cmdStr := range cmdStrs {
+ cmd := exec.Command("bash", "-c", cmdStr)
+ b, err := cmd.CombinedOutput()
+ if err != nil {
+ log.Println(string(b), err)
+ return err
+ }
+ }
+ return nil
+}
diff --git a/handler/link_tunnel.go b/handler/link_tunnel.go
new file mode 100644
index 0000000..b8c66ca
--- /dev/null
+++ b/handler/link_tunnel.go
@@ -0,0 +1,120 @@
+package handler
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+
+ "github.com/bjdgyc/anylink/common"
+)
+
+var hn string
+
+func init() {
+ // 获取主机名称
+ hn, _ = os.Hostname()
+}
+
+func LinkTunnel(w http.ResponseWriter, r *http.Request) {
+ // TODO 调试信息输出
+ // hd, _ := httputil.DumpRequest(r, true)
+ // fmt.Println("DumpRequest: ", string(hd))
+ // fmt.Println(r.RemoteAddr)
+
+ // 判断session-token的值
+ cookie, err := r.Cookie("webvpn")
+ if err != nil || cookie.Value == "" {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ sess := SToken2Sess(cookie.Value)
+ if sess == nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ // 开启link
+ sess.StartLink()
+ if sess.NetIp == nil {
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ // 客户端信息
+ cstp_mtu := r.Header.Get("X-CSTP-MTU")
+ master_Secret := r.Header.Get("X-DTLS-Master-Secret")
+ sess.MasterSecret = master_Secret
+ sess.Mtu = cstp_mtu
+ sess.RemoteAddr = r.RemoteAddr
+
+ w.Header().Set("Server", fmt.Sprintf("%s %s", common.APP_NAME, common.APP_VER))
+ w.Header().Set("X-CSTP-Version", "1")
+ w.Header().Set("X-CSTP-Protocol", "Copyright (c) 2004 Cisco Systems, Inc.")
+ w.Header().Set("X-CSTP-Address", sess.NetIp.String()) // 分配的ip地址
+ w.Header().Set("X-CSTP-Netmask", common.ServerCfg.Ipv4Netmask) // 子网掩码
+ w.Header().Set("X-CSTP-Hostname", hn) // 机器名称
+ for _, v := range common.ServerCfg.ClientDns {
+ w.Header().Add("X-CSTP-DNS", v) // dns地址
+ }
+ // 允许本地LAN访问vpn网络,必须放在路由的第一个
+ if common.ServerCfg.AllowLan {
+ w.Header().Set("X-CSTP-Split-Exclude", "0.0.0.0/255.255.255.255")
+ }
+ // 允许的路由
+ for _, v := range common.ServerCfg.Include {
+ w.Header().Add("X-CSTP-Split-Include", v)
+ }
+ // 不允许的路由
+ for _, v := range common.ServerCfg.Exclude {
+ w.Header().Add("X-CSTP-Split-Exclude", v)
+ }
+ // w.Header().Add("X-CSTP-Split-Include", "192.168.0.0/255.255.0.0")
+ // w.Header().Add("X-CSTP-Split-Exclude", "10.1.5.2/255.255.255.255")
+
+ w.Header().Set("X-CSTP-Lease-Duration", fmt.Sprintf("%d", common.IpLease)) // ip地址租期
+ w.Header().Set("X-CSTP-Session-Timeout", "none")
+ w.Header().Set("X-CSTP-Session-Timeout-Alert-Interval", "60")
+ w.Header().Set("X-CSTP-Session-Timeout-Remaining", "none")
+ w.Header().Set("X-CSTP-Idle-Timeout", "18000")
+ w.Header().Set("X-CSTP-Disconnected-Timeout", "18000")
+ w.Header().Set("X-CSTP-Keep", "true")
+ w.Header().Set("X-CSTP-Tunnel-All-DNS", "false")
+ w.Header().Set("X-CSTP-Rekey-Time", "5400")
+ w.Header().Set("X-CSTP-Rekey-Method", "new-tunnel")
+ w.Header().Set("X-CSTP-DPD", fmt.Sprintf("%d", common.ServerCfg.CstpDpd)) // 30 Dead peer detection in seconds
+ w.Header().Set("X-CSTP-Keepalive", fmt.Sprintf("%d", common.ServerCfg.CstpKeepalive)) // 20
+ w.Header().Set("X-CSTP-Banner", "welcome") // urlencode
+ w.Header().Set("X-CSTP-MSIE-Proxy-Lockdown", "true")
+ w.Header().Set("X-CSTP-Smartcard-Removal-Disconnect", "true")
+
+ w.Header().Set("X-CSTP-MTU", cstp_mtu) // 1399
+ w.Header().Set("X-DTLS-MTU", cstp_mtu)
+
+ w.Header().Set("X-DTLS-Session-ID", sess.DtlsSid)
+ w.Header().Set("X-DTLS-Port", "4433")
+ w.Header().Set("X-DTLS-Keepalive", fmt.Sprintf("%d", common.ServerCfg.CstpKeepalive))
+ w.Header().Set("X-DTLS-Rekey-Time", "5400")
+ w.Header().Set("X-DTLS12-CipherSuite", "ECDHE-ECDSA-AES128-GCM-SHA256")
+ // w.Header().Set("X-DTLS12-CipherSuite", "ECDHE-RSA-AES128-GCM-SHA256")
+
+ w.Header().Set("X-CSTP-License", "accept")
+ w.Header().Set("X-CSTP-Routing-Filtering-Ignore", "false")
+ w.Header().Set("X-CSTP-Quarantine", "false")
+ w.Header().Set("X-CSTP-Disable-Always-On-VPN", "false")
+ w.Header().Set("X-CSTP-Client-Bypass-Protocol", "false")
+ w.Header().Set("X-CSTP-TCP-Keepalive", "false")
+ // w.Header().Set("X-CSTP-Post-Auth-XML", ``)
+ w.WriteHeader(http.StatusOK)
+
+ hj := w.(http.Hijacker)
+ conn, _, err := hj.Hijack()
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // 开始数据处理
+ go LinkTun(sess)
+ go LinkCstp(conn, sess)
+}
diff --git a/handler/proto.go b/handler/proto.go
new file mode 100644
index 0000000..12b51ca
--- /dev/null
+++ b/handler/proto.go
@@ -0,0 +1,63 @@
+package handler
+
+type Payload struct {
+ ptype byte
+ data []byte
+}
+
+/*
+ var header = []byte{'S', 'T', 'F', 0x01, 0, 0, 0x00, 0}
+ https://tools.ietf.org/html/draft-mavrogiannopoulos-openconnect-02#section-2.2
+
+ +---------------------+---------------------------------------------+
+ | byte | value |
+ +---------------------+---------------------------------------------+
+ | 0 | fixed to 0x53 (S) |
+ | | |
+ | 1 | fixed to 0x54 (T) |
+ | | |
+ | 2 | fixed to 0x46 (F) |
+ | | |
+ | 3 | fixed to 0x01 |
+ | | |
+ | 4-5 | The length of the packet that follows this |
+ | | header in big endian order |
+ | | |
+ | 6 | The type of the payload that follows (see |
+ | | Table 3 for available types) |
+ | | |
+ | 7 | fixed to 0x00 |
+ +---------------------+---------------------------------------------+
+
+
+ The available payload types are listed in Table 3.
+ +---------------------+---------------------------------------------+
+ | Value | Description |
+ +---------------------+---------------------------------------------+
+ | 0x00 | DATA: the TLS record packet contains an |
+ | | IPv4 or IPv6 packet |
+ | | |
+ | 0x03 | DPD-REQ: used for dead peer detection. Once |
+ | | sent the peer should reply with a DPD-RESP |
+ | | packet, that has the same contents as the |
+ | | original request. |
+ | | |
+ | 0x04 | DPD-RESP: used as a response to a |
+ | | previously received DPD-REQ. |
+ | | |
+ | 0x05 | DISCONNECT: sent by the client (or server) |
+ | | to terminate the session. No data is |
+ | | associated with this request. The session |
+ | | will be invalidated after such request. |
+ | | |
+ | 0x07 | KEEPALIVE: sent by any peer. No data is |
+ | | associated with this request. |
+ | | |
+ | 0x08 | COMPRESSED DATA: a Data packet which is |
+ | | compressed prior to encryption. |
+ | | |
+ | 0x09 | TERMINATE: sent by the server to indicate |
+ | | that the server is shutting down. No data |
+ | | is associated with this request. |
+ +---------------------+---------------------------------------------+
+*/
diff --git a/handler/server.go b/handler/server.go
new file mode 100644
index 0000000..99450e5
--- /dev/null
+++ b/handler/server.go
@@ -0,0 +1,72 @@
+package handler
+
+import (
+ "crypto/tls"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "net/http/httputil"
+ _ "net/http/pprof"
+
+ "github.com/bjdgyc/anylink/common"
+ "github.com/julienschmidt/httprouter"
+)
+
+func Start() {
+ testTun()
+ go startDebug()
+ go startDtls()
+ go startTls()
+}
+
+func startDebug() {
+ http.ListenAndServe(common.ServerCfg.DebugAddr, nil)
+}
+
+func startTls() {
+ addr := common.ServerCfg.ServerAddr
+ certFile := common.ServerCfg.CertFile
+ keyFile := common.ServerCfg.CertKey
+
+ // 设置tls信息
+ tlsConfig := &tls.Config{
+ NextProtos: []string{"http/1.1"},
+ MinVersion: tls.VersionTLS12,
+ }
+ srv := &http.Server{
+ Addr: addr,
+ Handler: initRoute(),
+ TLSConfig: tlsConfig,
+ }
+
+ ln, err := net.Listen("tcp", addr)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer ln.Close()
+
+ srv.SetKeepAlivesEnabled(true)
+ fmt.Println("listen ", addr)
+ err = srv.ServeTLS(ln, certFile, keyFile)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func initRoute() http.Handler {
+ router := httprouter.New()
+ router.GET("/", checkVpnClient(LinkHome))
+ router.POST("/", checkVpnClient(LinkAuth))
+ router.HandlerFunc("CONNECT", "/CSCOSSLC/tunnel", LinkTunnel)
+ router.NotFound = http.HandlerFunc(notFound)
+ return router
+}
+
+func notFound(w http.ResponseWriter, r *http.Request) {
+ hu, _ := httputil.DumpRequest(r, true)
+ fmt.Println("NotFound: ", string(hu))
+
+ w.WriteHeader(http.StatusNotFound)
+ fmt.Fprintln(w, "404 page not found")
+}
diff --git a/handler/session.go b/handler/session.go
new file mode 100644
index 0000000..6bea917
--- /dev/null
+++ b/handler/session.go
@@ -0,0 +1,142 @@
+package handler
+
+import (
+ "fmt"
+ "log"
+ "math/rand"
+ "net"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/bjdgyc/anylink/common"
+)
+
+var (
+ sessMux = sync.Mutex{}
+ sessions = make(map[string]*Session) // session_token -> SessUser
+)
+
+type Session struct {
+ Sid string // auth返回的 session-id
+ Token string // session信息的唯一token
+ DtlsSid string // dtls协议的 session_id
+ MacAddr string // 客户端mac地址
+
+ // 开启link需要设置的参数
+ MasterSecret string // dtls协议的 master_secret
+ NetIp net.IP // 分配的ip地址
+ UserName string // 用户名
+ RemoteAddr string
+ Mtu string
+ TunName string
+ IsActive bool
+ LastLogin time.Time
+ closeOnce sync.Once
+ Closed chan struct{}
+ PayloadIn chan *Payload
+ PayloadOut chan *Payload
+}
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+
+ // 检测过期的session
+ go func() {
+ if common.ServerCfg.SessionTimeout == 0 {
+ return
+ }
+ timeout := time.Duration(common.ServerCfg.SessionTimeout) * time.Second
+ tick := time.Tick(time.Second * 30)
+ for range tick {
+ t := time.Now()
+ sessMux.Lock()
+ for k, v := range sessions {
+ if v.IsActive == true {
+ continue
+ }
+ if t.Sub(v.LastLogin) > timeout {
+ delete(sessions, k)
+ }
+ }
+ sessMux.Unlock()
+ }
+ }()
+}
+
+func NewSession() *Session {
+ // 生成32位的 token
+ btoken := make([]byte, 32)
+ rand.Read(btoken)
+
+ // 生成 dtls session_id
+ dtlsid := make([]byte, 32)
+ rand.Read(dtlsid)
+
+ token := fmt.Sprintf("%x", btoken)
+ sess := &Session{
+ Sid: fmt.Sprintf("%d", time.Now().Unix()),
+ Token: token,
+ DtlsSid: fmt.Sprintf("%x", dtlsid),
+ LastLogin: time.Now(),
+ }
+ sessMux.Lock()
+ defer sessMux.Unlock()
+ sessions[token] = sess
+ return sess
+}
+
+func (s *Session) StartLink() {
+ limit := common.LimitClient(s.UserName, false)
+ if limit == false {
+ s.NetIp = nil
+ return
+ }
+ s.NetIp = common.AcquireIp(s.MacAddr)
+ s.IsActive = true
+ s.closeOnce = sync.Once{}
+ s.Closed = make(chan struct{})
+ s.PayloadIn = make(chan *Payload)
+ s.PayloadOut = make(chan *Payload)
+}
+
+func (s *Session) Close() {
+ s.closeOnce.Do(func() {
+ log.Println("closeOnce")
+ close(s.Closed)
+ s.IsActive = false
+ s.LastLogin = time.Now()
+ common.ReleaseIp(s.NetIp, s.MacAddr)
+ common.LimitClient(s.UserName, true)
+ })
+}
+
+func SToken2Sess(stoken string) *Session {
+ stoken = strings.TrimSpace(stoken)
+ sarr := strings.Split(stoken, "@")
+ token := sarr[1]
+ sessMux.Lock()
+ defer sessMux.Unlock()
+ if sess, ok := sessions[token]; ok {
+ return sess
+ }
+
+ return nil
+}
+
+func Dtls2Sess(dtlsid []byte) *Session {
+ return nil
+}
+
+func DelSess(token string) {
+ delete(sessions, token)
+}
+
+func DelSessByStoken(stoken string) {
+ stoken = strings.TrimSpace(stoken)
+ sarr := strings.Split(stoken, "@")
+ token := sarr[1]
+ sessMux.Lock()
+ defer sessMux.Unlock()
+ delete(sessions, token)
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..6ba068b
--- /dev/null
+++ b/main.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/bjdgyc/anylink/common"
+ "github.com/bjdgyc/anylink/handler"
+)
+
+var COMMIT_ID string
+
+func main() {
+ common.CommitId = COMMIT_ID
+ common.InitConfig()
+ handler.Start()
+ signalWatch()
+}
+
+func signalWatch() {
+ fmt.Println("Server pid: ", os.Getpid())
+
+ sigs := make(chan os.Signal, 1)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGALRM, syscall.SIGUSR2)
+ for {
+ sig := <-sigs
+ fmt.Printf("Get signal: %v \n", sig)
+ switch sig {
+ case syscall.SIGUSR2:
+ // reload
+ fmt.Println("reload")
+ default:
+ // stop
+ return
+ }
+ }
+}