更改目录结构

This commit is contained in:
bjdgyc
2021-03-01 15:46:08 +08:00
parent 3464d1d10e
commit 0f91c779e3
105 changed files with 29099 additions and 96 deletions

19
server/.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
vendor/
ui/
.idea/
anylink

79
server/admin/api_base.go Normal file
View File

@@ -0,0 +1,79 @@
package admin
import (
"fmt"
"net/http"
"time"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/pkg/utils"
"github.com/gorilla/mux"
)
// Login 登陆接口
func Login(w http.ResponseWriter, r *http.Request) {
// TODO 调试信息输出
// hd, _ := httputil.DumpRequest(r, true)
// fmt.Println("DumpRequest: ", string(hd))
_ = r.ParseForm()
adminUser := r.PostFormValue("admin_user")
adminPass := r.PostFormValue("admin_pass")
// 认证错误
if !(adminUser == base.Cfg.AdminUser &&
utils.PasswordVerify(adminPass, base.Cfg.AdminPass)) {
RespError(w, RespUserOrPassErr)
return
}
// token有效期
expiresAt := time.Now().Unix() + 3600*3
jwtData := map[string]interface{}{"admin_user": adminUser}
tokenString, err := SetJwtData(jwtData, expiresAt)
if err != nil {
RespError(w, 1, err)
return
}
data := make(map[string]interface{})
data["token"] = tokenString
data["admin_user"] = adminUser
data["expires_at"] = expiresAt
RespSucess(w, data)
}
func authMiddleware(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "*")
if r.Method == http.MethodOptions {
return
}
route := mux.CurrentRoute(r)
name := route.GetName()
// fmt.Println("bb", r.URL.Path, name)
if utils.InArrStr([]string{"login", "index", "static"}, name) {
// 不进行鉴权
next.ServeHTTP(w, r)
return
}
// 进行登陆鉴权
jwtToken := r.Header.Get("Jwt")
if jwtToken == "" {
jwtToken = r.FormValue("jwt")
}
data, err := GetJwtData(jwtToken)
if err != nil || base.Cfg.AdminUser != fmt.Sprint(data["admin_user"]) {
w.WriteHeader(http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

108
server/admin/api_group.go Normal file
View File

@@ -0,0 +1,108 @@
package admin
import (
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
"github.com/bjdgyc/anylink/dbdata"
)
func GroupList(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
pageS := r.FormValue("page")
page, _ := strconv.Atoi(pageS)
if page < 1 {
page = 1
}
var pageSize = dbdata.PageSize
count := dbdata.CountAll(&dbdata.Group{})
var datas []dbdata.Group
err := dbdata.All(&datas, pageSize, page)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
data := map[string]interface{}{
"count": count,
"page_size": pageSize,
"datas": datas,
}
RespSucess(w, data)
}
func GroupNames(w http.ResponseWriter, r *http.Request) {
var names = dbdata.GetGroupNames()
data := map[string]interface{}{
"count": len(names),
"page_size": 0,
"datas": names,
}
RespSucess(w, data)
}
func GroupDetail(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
idS := r.FormValue("id")
id, _ := strconv.Atoi(idS)
if id < 1 {
RespError(w, RespParamErr, "Id错误")
return
}
var data dbdata.Group
err := dbdata.One("Id", id, &data)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, data)
}
func GroupSet(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
defer r.Body.Close()
v := &dbdata.Group{}
err = json.Unmarshal(body, v)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
err = dbdata.SetGroup(v)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, nil)
}
func GroupDel(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
idS := r.FormValue("id")
id, _ := strconv.Atoi(idS)
if id < 1 {
RespError(w, RespParamErr, "Id错误")
return
}
data := dbdata.Group{Id: id}
err := dbdata.Del(&data)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, nil)
}

111
server/admin/api_ip_map.go Normal file
View File

@@ -0,0 +1,111 @@
package admin
import (
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
"time"
"github.com/bjdgyc/anylink/dbdata"
)
func UserIpMapList(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
pageS := r.FormValue("page")
page, _ := strconv.Atoi(pageS)
if page < 1 {
page = 1
}
var pageSize = dbdata.PageSize
count := dbdata.CountAll(&dbdata.IpMap{})
var datas []dbdata.IpMap
err := dbdata.All(&datas, pageSize, page)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
data := map[string]interface{}{
"count": count,
"page_size": pageSize,
"datas": datas,
}
RespSucess(w, data)
}
func UserIpMapDetail(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
idS := r.FormValue("id")
id, _ := strconv.Atoi(idS)
if id < 1 {
RespError(w, RespParamErr, "用户名错误")
return
}
var data dbdata.IpMap
err := dbdata.One("Id", id, &data)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, data)
}
func UserIpMapSet(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
defer r.Body.Close()
v := &dbdata.IpMap{}
err = json.Unmarshal(body, v)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
// fmt.Println(v, len(v.Ip), len(v.MacAddr))
if len(v.IpAddr) < 4 || len(v.MacAddr) < 6 {
RespError(w, RespParamErr, "IP或MAC错误")
return
}
v.UpdatedAt = time.Now()
err = dbdata.Save(v)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, nil)
}
func UserIpMapDel(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
idS := r.FormValue("id")
id, _ := strconv.Atoi(idS)
if id < 1 {
RespError(w, RespParamErr, "IP映射id错误")
return
}
data := dbdata.IpMap{Id: id}
err := dbdata.Del(&data)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, nil)
}

62
server/admin/api_other.go Normal file
View File

@@ -0,0 +1,62 @@
package admin
import (
"encoding/json"
"io/ioutil"
"net/http"
"github.com/bjdgyc/anylink/dbdata"
)
func setOtherGet(data interface{}, w http.ResponseWriter) {
err := dbdata.SettingGet(data)
if err != nil && !dbdata.CheckErrNotFound(err) {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, data)
}
func setOtherEdit(data interface{}, w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
defer r.Body.Close()
err = json.Unmarshal(body, data)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
// fmt.Println(data)
err = dbdata.SettingSet(data)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, data)
}
func SetOtherSmtp(w http.ResponseWriter, r *http.Request) {
data := &dbdata.SettingSmtp{}
setOtherGet(data, w)
}
func SetOtherSmtpEdit(w http.ResponseWriter, r *http.Request) {
data := &dbdata.SettingSmtp{}
setOtherEdit(data, w, r)
}
func SetOther(w http.ResponseWriter, r *http.Request) {
data := &dbdata.SettingOther{}
setOtherGet(data, w)
}
func SetOtherEdit(w http.ResponseWriter, r *http.Request) {
data := &dbdata.SettingOther{}
setOtherEdit(data, w, r)
}

93
server/admin/api_set.go Normal file
View File

@@ -0,0 +1,93 @@
package admin
import (
"fmt"
"net/http"
"runtime"
"github.com/bjdgyc/anylink/dbdata"
"github.com/bjdgyc/anylink/sessdata"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/pkg/utils"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/host"
"github.com/shirou/gopsutil/load"
"github.com/shirou/gopsutil/mem"
)
func SetHome(w http.ResponseWriter, r *http.Request) {
data := make(map[string]interface{})
sess := sessdata.OnlineSess()
data["counts"] = map[string]int{
"online": len(sess),
"user": dbdata.CountAll(&dbdata.User{}),
"group": dbdata.CountAll(&dbdata.Group{}),
"ip_map": dbdata.CountAll(&dbdata.IpMap{}),
}
RespSucess(w, data)
}
func SetSystem(w http.ResponseWriter, r *http.Request) {
data := make(map[string]interface{})
m, _ := mem.VirtualMemory()
data["mem"] = map[string]interface{}{
"total": utils.HumanByte(m.Total),
"free": utils.HumanByte(m.Free),
"percent": decimal(m.UsedPercent),
}
d, _ := disk.Usage("/")
data["disk"] = map[string]interface{}{
"total": utils.HumanByte(d.Total),
"free": utils.HumanByte(d.Free),
"percent": decimal(d.UsedPercent),
}
cc, _ := cpu.Counts(true)
c, _ := cpu.Info()
ci := c[0]
cpuUsedPercent, _ := cpu.Percent(0, false)
cup := cpuUsedPercent[0]
if cup == 0 {
cup = 1
}
data["cpu"] = map[string]interface{}{
"core": cc,
"modelName": ci.ModelName,
"ghz": fmt.Sprintf("%.2f GHz", ci.Mhz/1000),
"percent": decimal(cup),
}
hi, _ := host.Info()
l, _ := load.Avg()
data["sys"] = map[string]interface{}{
"goOs": runtime.GOOS,
"goArch": runtime.GOARCH,
"goVersion": runtime.Version(),
"goroutine": runtime.NumGoroutine(),
"hostname": hi.Hostname,
"platform": fmt.Sprintf("%v %v %v", hi.Platform, hi.PlatformFamily, hi.PlatformVersion),
"kernel": hi.KernelVersion,
"load": fmt.Sprint(l.Load1, l.Load5, l.Load15),
}
RespSucess(w, data)
}
func SetSoft(w http.ResponseWriter, r *http.Request) {
data := base.ServerCfg2Slice()
RespSucess(w, data)
}
func decimal(f float64) float64 {
i := int(f * 100)
return float64(i) / 100
}

245
server/admin/api_user.go Normal file
View File

@@ -0,0 +1,245 @@
package admin
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"text/template"
"time"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata"
"github.com/bjdgyc/anylink/sessdata"
"github.com/skip2/go-qrcode"
)
func UserList(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
prefix := r.FormValue("prefix")
pageS := r.FormValue("page")
page, _ := strconv.Atoi(pageS)
if page < 1 {
page = 1
}
var (
pageSize = dbdata.PageSize
count int
datas []dbdata.User
err error
)
// 查询前缀匹配
if len(prefix) > 0 {
count = pageSize
err = dbdata.Prefix("Username", prefix, &datas, pageSize, 1)
} else {
count = dbdata.CountAll(&dbdata.User{})
err = dbdata.All(&datas, pageSize, page)
}
if err != nil && !dbdata.CheckErrNotFound(err) {
RespError(w, RespInternalErr, err)
return
}
data := map[string]interface{}{
"count": count,
"page_size": pageSize,
"datas": datas,
}
RespSucess(w, data)
}
func UserDetail(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
idS := r.FormValue("id")
id, _ := strconv.Atoi(idS)
if id < 1 {
RespError(w, RespParamErr, "用户名错误")
return
}
var user dbdata.User
err := dbdata.One("Id", id, &user)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, user)
}
func UserSet(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
defer r.Body.Close()
data := &dbdata.User{}
err = json.Unmarshal(body, data)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
err = dbdata.SetUser(data)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
// 发送邮件
if data.SendEmail {
err = userAccountMail(data)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
}
RespSucess(w, nil)
}
func UserDel(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
idS := r.FormValue("id")
id, _ := strconv.Atoi(idS)
if id < 1 {
RespError(w, RespParamErr, "用户id错误")
return
}
user := dbdata.User{Id: id}
err := dbdata.Del(&user)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
RespSucess(w, nil)
}
func UserOtpQr(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
b64 := r.FormValue("b64")
idS := r.FormValue("id")
id, _ := strconv.Atoi(idS)
var user dbdata.User
err := dbdata.One("Id", id, &user)
if err != nil {
RespError(w, RespInternalErr, err)
return
}
issuer := base.Cfg.Issuer
qrstr := fmt.Sprintf("otpauth://totp/%s:%s?issuer=%s&secret=%s", issuer, user.Email, issuer, user.OtpSecret)
qr, _ := qrcode.New(qrstr, qrcode.High)
if b64 == "1" {
data, _ := qr.PNG(300)
s := base64.StdEncoding.EncodeToString(data)
_, err = fmt.Fprint(w, s)
if err != nil {
base.Error(err)
}
return
}
err = qr.Write(300, w)
if err != nil {
base.Error(err)
}
}
// 在线用户
func UserOnline(w http.ResponseWriter, r *http.Request) {
datas := sessdata.OnlineSess()
data := map[string]interface{}{
"count": len(datas),
"page_size": dbdata.PageSize,
"datas": datas,
}
RespSucess(w, data)
}
func UserOffline(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
token := r.FormValue("token")
sessdata.CloseSess(token)
RespSucess(w, nil)
}
func UserReline(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
token := r.FormValue("token")
sessdata.CloseCSess(token)
RespSucess(w, nil)
}
type userAccountMailData struct {
Issuer string
LinkAddr string
Group string
Username string
PinCode string
OtpImg string
}
func userAccountMail(user *dbdata.User) error {
// 平台通知
htmlBody := `
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Hello AnyLink!</title>
</head>
<body>
%s
</body>
</html>
`
dataOther := &dbdata.SettingOther{}
err := dbdata.SettingGet(dataOther)
if err != nil {
base.Error(err)
return err
}
htmlBody = fmt.Sprintf(htmlBody, dataOther.AccountMail)
// fmt.Println(htmlBody)
// token有效期3天
expiresAt := time.Now().Unix() + 3600*24*3
jwtData := map[string]interface{}{"id": user.Id}
tokenString, err := SetJwtData(jwtData, expiresAt)
if err != nil {
return err
}
data := userAccountMailData{
LinkAddr: base.Cfg.LinkAddr,
Group: strings.Join(user.Groups, ","),
Username: user.Username,
PinCode: user.PinCode,
OtpImg: fmt.Sprintf("https://%s/otp_qr?id=%d&jwt=%s", base.Cfg.LinkAddr, user.Id, tokenString),
}
w := bytes.NewBufferString("")
t, _ := template.New("auth_complete").Parse(htmlBody)
err = t.Execute(w, data)
if err != nil {
return err
}
// fmt.Println(w.String())
return SendMail(base.Cfg.Issuer+"平台通知", user.Email, w.String())
}

108
server/admin/common.go Normal file
View File

@@ -0,0 +1,108 @@
package admin
import (
"crypto/tls"
"errors"
"time"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata"
"github.com/dgrijalva/jwt-go"
mail "github.com/xhit/go-simple-mail/v2"
// "github.com/mojocn/base64Captcha"
)
func SetJwtData(data map[string]interface{}, expiresAt int64) (string, error) {
jwtData := jwt.MapClaims{"exp": expiresAt}
for k, v := range data {
jwtData[k] = v
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtData)
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString([]byte(base.Cfg.JwtSecret))
return tokenString, err
}
func GetJwtData(jwtToken string) (map[string]interface{}, error) {
token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) {
// since we only use the one private key to sign the tokens,
// we also only use its public counter part to verify
return []byte(base.Cfg.JwtSecret), nil
})
if err != nil || !token.Valid {
return nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, errors.New("data is parse err")
}
return claims, nil
}
func SendMail(subject, to, htmlBody string) error {
dataSmtp := &dbdata.SettingSmtp{}
err := dbdata.SettingGet(dataSmtp)
if err != nil {
base.Error(err)
return err
}
server := mail.NewSMTPClient()
// SMTP Server
server.Host = dataSmtp.Host
server.Port = dataSmtp.Port
server.Username = dataSmtp.Username
server.Password = dataSmtp.Password
if dataSmtp.UseSSl {
server.Encryption = mail.EncryptionSSL
}
// Since v2.3.0 you can specified authentication type:
// - PLAIN (default)
// - LOGIN
// - CRAM-MD5
server.Authentication = mail.AuthPlain
// Variable to keep alive connection
server.KeepAlive = false
// Timeout for connect to SMTP Server
server.ConnectTimeout = 10 * time.Second
// Timeout for send the data and wait respond
server.SendTimeout = 10 * time.Second
// Set TLSConfig to provide custom TLS configuration. For example,
// to skip TLS verification (useful for testing):
server.TLSConfig = &tls.Config{InsecureSkipVerify: true}
// SMTP client
smtpClient, err := server.Connect()
if err != nil {
base.Error(err)
return err
}
// New email simple html with inline and CC
email := mail.NewMSG()
email.SetFrom(dataSmtp.From).
AddTo(to).
SetSubject(subject)
email.SetBody(mail.TextHTML, htmlBody)
// Call Send and pass the client
err = email.Send(smtpClient)
if err != nil {
base.Error(err)
}
return err
}

View File

@@ -0,0 +1,23 @@
package admin
import (
"testing"
"time"
"github.com/bjdgyc/anylink/base"
"github.com/stretchr/testify/assert"
)
func TestJwtData(t *testing.T) {
assert := assert.New(t)
base.Cfg.JwtSecret = "dsfasfdfsadfasdfasd3sdaf"
data := map[string]interface{}{
"key": "value",
}
expiresAt := time.Now().Add(time.Minute).Unix()
token, err := SetJwtData(data, expiresAt)
assert.Nil(err)
dataN, err := GetJwtData(token)
assert.Nil(err)
assert.Equal(dataN["key"], "value")
}

15
server/admin/error.go Normal file
View File

@@ -0,0 +1,15 @@
package admin
// 返回码
const (
RespSuccess = 0
RespInternalErr = 1
RespTokenErr = 2
RespUserOrPassErr = 3
RespParamErr = 4
)
var RespMap = map[int]string{
RespTokenErr: "客户端TOKEN错误",
RespUserOrPassErr: "用户名或密码错误",
}

64
server/admin/resp.go Normal file
View File

@@ -0,0 +1,64 @@
package admin
import (
"encoding/json"
"fmt"
"net/http"
"runtime"
"github.com/bjdgyc/anylink/base"
)
type Resp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Location string `json:"location"`
Data interface{} `json:"data"`
}
func respHttp(w http.ResponseWriter, respCode int, data interface{}, errS ...interface{}) {
resp := Resp{
Code: respCode,
Msg: "success",
Data: data,
}
_, file, line, _ := runtime.Caller(2)
resp.Location = fmt.Sprintf("%v:%v", file, line)
if respCode != 0 {
resp.Msg = ""
if v, ok := RespMap[respCode]; ok {
resp.Msg += v
}
if len(errS) > 0 {
resp.Msg += fmt.Sprint(errS...)
}
}
b, err := json.Marshal(resp)
if err != nil {
base.Error(err, resp)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, err = w.Write(b)
if err != nil {
base.Error(err)
}
// 记录返回数据
// logger.Category("response").Debug(string(b))
}
func RespSucess(w http.ResponseWriter, data interface{}) {
respHttp(w, 0, data, "")
}
func RespError(w http.ResponseWriter, respCode int, errS ...interface{}) {
respHttp(w, respCode, nil, errS...)
}
func RespData(w http.ResponseWriter, data interface{}, err error) {
respHttp(w, http.StatusOK, data, "")
}

39
server/admin/resp_test.go Normal file
View File

@@ -0,0 +1,39 @@
package admin
import (
"encoding/json"
"io/ioutil"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRespSucess(t *testing.T) {
assert := assert.New(t)
w := httptest.NewRecorder()
RespSucess(w, "data")
// fmt.Println(w)
assert.Equal(w.Code, 200)
body, _ := ioutil.ReadAll(w.Body)
res := Resp{}
err := json.Unmarshal(body, &res)
assert.Nil(err)
assert.Equal(res.Code, 0)
assert.Equal(res.Data, "data")
}
func TestRespError(t *testing.T) {
assert := assert.New(t)
w := httptest.NewRecorder()
RespError(w, 10, "err-msg")
// fmt.Println(w)
assert.Equal(w.Code, 200)
body, _ := ioutil.ReadAll(w.Body)
res := Resp{}
err := json.Unmarshal(body, &res)
assert.Nil(err)
assert.Equal(res.Code, 10)
assert.Equal(res.Msg, "err-msg")
}

71
server/admin/server.go Normal file
View File

@@ -0,0 +1,71 @@
// admin:后台管理接口
package admin
import (
"net/http"
"net/http/pprof"
"github.com/bjdgyc/anylink/base"
"github.com/gorilla/mux"
)
// 开启服务
func StartAdmin() {
r := mux.NewRouter()
r.Use(authMiddleware)
r.Handle("/", http.RedirectHandler("/ui/", http.StatusFound)).Name("index")
r.PathPrefix("/ui/").Handler(
http.StripPrefix("/ui/", http.FileServer(http.Dir(base.Cfg.UiPath))),
).Name("static")
r.HandleFunc("/base/login", Login).Name("login")
r.HandleFunc("/set/home", SetHome)
r.HandleFunc("/set/system", SetSystem)
r.HandleFunc("/set/soft", SetSoft)
r.HandleFunc("/set/other", SetOther)
r.HandleFunc("/set/other/edit", SetOtherEdit)
r.HandleFunc("/set/other/smtp", SetOtherSmtp)
r.HandleFunc("/set/other/smtp/edit", SetOtherSmtpEdit)
r.HandleFunc("/user/list", UserList)
r.HandleFunc("/user/detail", UserDetail)
r.HandleFunc("/user/set", UserSet)
r.HandleFunc("/user/del", UserDel)
r.HandleFunc("/user/online", UserOnline)
r.HandleFunc("/user/offline", UserOffline)
r.HandleFunc("/user/reline", UserReline)
r.HandleFunc("/user/otp_qr", UserOtpQr)
r.HandleFunc("/user/ip_map/list", UserIpMapList)
r.HandleFunc("/user/ip_map/detail", UserIpMapDetail)
r.HandleFunc("/user/ip_map/set", UserIpMapSet)
r.HandleFunc("/user/ip_map/del", UserIpMapDel)
r.HandleFunc("/group/list", GroupList)
r.HandleFunc("/group/names", GroupNames)
r.HandleFunc("/group/detail", GroupDetail)
r.HandleFunc("/group/set", GroupSet)
r.HandleFunc("/group/del", GroupDel)
// pprof
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
r.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
r.HandleFunc("/debug/pprof/trace", pprof.Trace)
r.HandleFunc("/debug/pprof", location("/debug/pprof/"))
r.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
base.Info("Listen admin", base.Cfg.AdminAddr)
err := http.ListenAndServe(base.Cfg.AdminAddr, r)
if err != nil {
base.Fatal(err)
}
}
func location(url string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", url)
w.WriteHeader(http.StatusFound)
}
}

6
server/base/app_ver.go Normal file
View File

@@ -0,0 +1,6 @@
package base
const (
APP_NAME = "AnyLink"
APP_VER = "0.1.6"
)

135
server/base/cfg_server.go Normal file
View File

@@ -0,0 +1,135 @@
package base
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/pelletier/go-toml"
)
const (
LinkModeTUN = "tun"
LinkModeTAP = "tap"
)
var (
Cfg = &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 {
LinkAddr string `toml:"link_addr" info:"vpn服务对外地址"`
ServerAddr string `toml:"server_addr" info:"前台服务监听地址"`
AdminAddr string `toml:"admin_addr" info:"后台服务监听地址"`
ProxyProtocol bool `toml:"proxy_protocol" info:"TCP代理协议"`
DbFile string `toml:"db_file" info:"数据库地址"`
CertFile string `toml:"cert_file" info:"证书文件"`
CertKey string `toml:"cert_key" info:"证书密钥"`
UiPath string `toml:"ui_path" info:"ui文件路径"`
FilesPath string `toml:"files_path" info:"外部下载文件路径"`
LogPath string `toml:"log_path" info:"日志文件路径"`
LogLevel string `toml:"log_level" info:"日志等级"`
Issuer string `toml:"issuer" info:"系统名称"`
AdminUser string `toml:"admin_user" info:"管理用户名"`
AdminPass string `toml:"admin_pass" info:"管理用户密码"`
JwtSecret string `toml:"jwt_secret" info:"JWT密钥"`
LinkMode string `toml:"link_mode" info:"虚拟网络类型"` // tun tap
Ipv4CIDR string `toml:"ipv4_cidr" info:"ip地址网段"` // 192.168.1.0/24
Ipv4Gateway string `toml:"ipv4_gateway" info:"ipv4_gateway"`
Ipv4Pool []string `toml:"ipv4_pool" info:"IPV4起止地址池"` // Pool[0]=192.168.1.100 Pool[1]=192.168.1.200
IpLease int `toml:"ip_lease" info:"IP租期(秒)"`
MaxClient int `toml:"max_client" info:"最大用户连接"`
MaxUserClient int `toml:"max_user_client" info:"最大单用户连接"`
DefaultGroup string `toml:"default_group" info:"默认用户组"`
CstpKeepalive int `toml:"cstp_keepalive" info:"keepalive时间(秒)"` // in seconds
CstpDpd int `toml:"cstp_dpd" info:"死链接检测时间(秒)"` // Dead peer detection in seconds
MobileKeepalive int `toml:"mobile_keepalive" info:"移动端keepalive接检测时间(秒)"`
MobileDpd int `toml:"mobile_dpd" info:"移动端死链接检测时间(秒)"`
SessionTimeout int `toml:"session_timeout" info:"session过期时间(秒)"` // in seconds
AuthTimeout int `toml:"auth_timeout" info:"auth_timeout"` // in seconds
}
func initServerCfg() {
b, err := ioutil.ReadFile(serverFile)
if err != nil {
panic(err)
}
err = toml.Unmarshal(b, Cfg)
if err != nil {
panic(err)
}
sf, _ := filepath.Abs(serverFile)
base := filepath.Dir(sf)
// 转换成绝对路径
Cfg.DbFile = getAbsPath(base, Cfg.DbFile)
Cfg.CertFile = getAbsPath(base, Cfg.CertFile)
Cfg.CertKey = getAbsPath(base, Cfg.CertKey)
Cfg.UiPath = getAbsPath(base, Cfg.UiPath)
Cfg.FilesPath = getAbsPath(base, Cfg.FilesPath)
Cfg.LogPath = getAbsPath(base, Cfg.LogPath)
if len(Cfg.JwtSecret) < 20 {
fmt.Println("请设置 jwt_secret 长度20位以上")
os.Exit(0)
}
fmt.Printf("ServerCfg: %+v \n", Cfg)
}
func getAbsPath(base, cfile string) string {
if cfile == "" {
return ""
}
abs := filepath.IsAbs(cfile)
if abs {
return cfile
}
return filepath.Join(base, cfile)
}
type SCfg struct {
Name string `json:"name"`
Info string `json:"info"`
Data interface{} `json:"data"`
}
func ServerCfg2Slice() []SCfg {
ref := reflect.ValueOf(Cfg)
s := ref.Elem()
var datas []SCfg
typ := s.Type()
numFields := s.NumField()
for i := 0; i < numFields; i++ {
field := typ.Field(i)
value := s.Field(i)
tag := field.Tag.Get("toml")
tags := strings.Split(tag, ",")
info := field.Tag.Get("info")
datas = append(datas, SCfg{Name: tags[0], Info: info, Data: value.Interface()})
}
return datas
}

54
server/base/flag.go Normal file
View File

@@ -0,0 +1,54 @@
package base
import (
"flag"
"fmt"
"math/rand"
"os"
"runtime"
"strings"
"time"
"github.com/bjdgyc/anylink/pkg/utils"
)
var (
// 提交id
CommitId string
// 配置文件
serverFile string
// pass明文
passwd string
// 生成密钥
secret bool
// 显示版本信息
rev bool
)
func initFlag() {
flag.StringVar(&serverFile, "conf", "./conf/server.toml", "server config files path")
flag.StringVar(&passwd, "passwd", "", "convert the password plaintext")
flag.BoolVar(&secret, "secret", false, "generate a random jwt secret")
flag.BoolVar(&rev, "rev", false, "display version info")
flag.Parse()
if passwd != "" {
pass, _ := utils.PasswordHash(passwd)
fmt.Printf("Passwd:%s\n", pass)
os.Exit(0)
}
if secret {
rand.Seed(time.Now().UnixNano())
s, _ := utils.RandSecret(40, 60)
s = strings.Trim(s, "=")
fmt.Printf("Secret:%s\n", s)
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)
}
}

146
server/base/log.go Normal file
View File

@@ -0,0 +1,146 @@
package base
import (
"fmt"
"log"
"os"
"path"
"strings"
"time"
)
const (
_Debug = iota
_Info
_Warn
_Error
_Fatal
)
var (
baseLog *log.Logger
baseLevel int
levels map[int]string
dateFormat = "2006-01-02"
logName = "anylink.log"
)
// 实现 os.Writer 接口
type logWriter struct {
UseStdout bool
FileName string
File *os.File
NowDate string
}
// 实现日志文件的切割
func (lw *logWriter) Write(p []byte) (n int, err error) {
if !lw.UseStdout {
return lw.File.Write(p)
}
date := time.Now().Format(dateFormat)
if lw.NowDate != date {
_ = lw.File.Close()
_ = os.Rename(lw.FileName, lw.FileName+"."+lw.NowDate)
lw.NowDate = date
lw.newFile()
}
return lw.File.Write(p)
}
// 创建新文件
func (lw *logWriter) newFile() {
if lw.UseStdout {
lw.File = os.Stdout
return
}
f, err := os.OpenFile(lw.FileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic(err)
}
lw.File = f
}
func initLog() {
// 初始化 baseLog
baseLw := &logWriter{
UseStdout: Cfg.LogPath == "",
FileName: path.Join(Cfg.LogPath, logName),
NowDate: time.Now().Format(dateFormat),
}
baseLw.newFile()
baseLevel = logLevel2Int(Cfg.LogLevel)
baseLog = log.New(baseLw, "", log.LstdFlags|log.Lshortfile)
}
// 获取 log.Logger
func GetBaseLog() *log.Logger {
return baseLog
}
func logLevel2Int(l string) int {
levels = map[int]string{
_Debug: "Debug",
_Info: "Info",
_Warn: "Warn",
_Error: "Error",
_Fatal: "Fatal",
}
lvl := _Info
for k, v := range levels {
if strings.EqualFold(strings.ToLower(l), strings.ToLower(v)) {
lvl = k
}
}
return lvl
}
func output(l int, s ...interface{}) {
lvl := fmt.Sprintf("[%s] ", levels[l])
_ = baseLog.Output(3, lvl+fmt.Sprintln(s...))
}
func Debug(v ...interface{}) {
l := _Debug
if baseLevel > l {
return
}
output(l, v...)
}
func Info(v ...interface{}) {
l := _Info
if baseLevel > l {
return
}
output(l, v...)
}
func Warn(v ...interface{}) {
l := _Warn
if baseLevel > l {
return
}
output(l, v...)
}
func Error(v ...interface{}) {
l := _Error
if baseLevel > l {
return
}
output(l, v...)
}
func Fatal(v ...interface{}) {
l := _Fatal
if baseLevel > l {
return
}
output(l, v...)
os.Exit(1)
}

11
server/base/start.go Normal file
View File

@@ -0,0 +1,11 @@
package base
func Start() {
initFlag()
initServerCfg()
initLog()
}
func Test() {
initLog()
}

38
server/bridge-init.sh Normal file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
#################################
# Set up Ethernet bridge on Linux
# Requires: bridge-utils
#################################
# Define Bridge Interface
br="anylink0"
# Define physical ethernet interface to be bridged
# with TAP interface(s) above.
eth="eth0"
eth_ip="192.168.10.4"
eth_netmask="255.255.255.0"
eth_broadcast="192.168.10.255"
eth_gateway="192.168.10.1"
brctl addbr $br
brctl addif $br $eth
ifconfig $eth 0.0.0.0 up
mac=`cat /sys/class/net/$eth/address`
ifconfig $br hw ether $mac
ifconfig $br $eth_ip netmask $eth_netmask broadcast $eth_broadcast up
route add default gateway $eth_gateway

65
server/conf/server.toml Normal file
View File

@@ -0,0 +1,65 @@
#服务配置信息
#其他配置文件,可以使用绝对路径
#或者相对于server.toml的路径
#数据文件
db_file = "./data.db"
#证书文件
cert_file = "./vpn_cert.pem"
cert_key = "./vpn_cert.key"
ui_path = "../ui"
files_path = "../files"
#日志目录,为空写入标准输出
log_path = "../log"
log_level = "info"
#系统名称
issuer = "XX公司VPN"
#后台管理用户
admin_user = "admin"
#pass 123456
admin_pass = "$2a$10$UQ7C.EoPifDeJh6d8.31TeSPQU7hM/NOM2nixmBucJpAuXDQNqNke"
jwt_secret = ""
#vpn服务对外地址,影响开通邮件二维码
link_addr = "vpn.xx.com"
#前台服务监听地址
server_addr = ":443"
#后台服务监听地址
admin_addr = ":8800"
#开启tcp proxy protocol协议
proxy_protocol = false
link_mode = "tun"
#客户端分配的ip地址池
ipv4_cidr = "192.168.10.0/24"
ipv4_gateway = "192.168.10.1"
ipv4_pool = ["192.168.10.100", "192.168.10.200"]
#最大客户端数量
max_client = 100
#单个用户同时在线数量
max_user_client = 3
#IP租期(秒)
ip_lease = 1209600
#默认选择的组
default_group = "one"
#客户端失效检测时间(秒) dpd > keepalive
cstp_keepalive = 20
cstp_dpd = 30
mobile_keepalive = 50
mobile_dpd = 60
#session过期时间用于断线重连0永不过期
session_timeout = 3600
auth_timeout = 0

89
server/dbdata/db.go Normal file
View File

@@ -0,0 +1,89 @@
package dbdata
import (
"time"
"github.com/asdine/storm/v3"
"github.com/asdine/storm/v3/codec/json"
"github.com/bjdgyc/anylink/base"
bolt "go.etcd.io/bbolt"
)
var (
sdb *storm.DB
)
func initDb() {
var err error
sdb, err = storm.Open(base.Cfg.DbFile, storm.Codec(json.Codec),
storm.BoltOptions(0600, &bolt.Options{Timeout: 10 * time.Second}))
if err != nil {
base.Fatal(err)
}
// 初始化数据库
err = sdb.Init(&User{})
if err != nil {
base.Fatal(err)
}
// fmt.Println("s1")
}
func initData() {
var (
err error
install bool
)
// 判断是否初次使用
err = Get(SettingBucket, Installed, &install)
if err == nil && install {
// 已经安装过
return
}
defer func() {
_ = Set(SettingBucket, Installed, true)
}()
smtp := &SettingSmtp{
Host: "127.0.0.1",
Port: 25,
From: "vpn@xx.com",
}
_ = SettingSet(smtp)
other := &SettingOther{
Banner: "您已接入公司网络,请按照公司规定使用。\n请勿进行非工作下载及视频行为",
AccountMail: accountMail,
}
_ = SettingSet(other)
}
func CheckErrNotFound(err error) bool {
return err == storm.ErrNotFound
}
const accountMail = `<p>您好:</p>
<p>&nbsp;&nbsp;您的{{.Issuer}}账号已经审核开通。</p>
<p>
登陆地址: <b>{{.LinkAddr}}</b> <br/>
用户组: <b>{{.Group}}</b> <br/>
用户名: <b>{{.Username}}</b> <br/>
用户PIN码: <b>{{.PinCode}}</b> <br/>
用户动态码(3天后失效):<br/>
<img src="{{.OtpImg}}"/>
</p>
<div>
使用说明:
<ul>
<li>请使用OTP软件扫描动态码二维码</li>
<li>然后使用anyconnect客户端进行登陆</li>
<li>登陆密码为 【PIN码+动态码】</li>
</ul>
</div>
<p>
软件下载地址: https://gitee.com/bjdgyc/anylink-soft/blob/master/README.md
</p>`

66
server/dbdata/db_orm.go Normal file
View File

@@ -0,0 +1,66 @@
package dbdata
import "github.com/asdine/storm/v3/index"
const PageSize = 10
func Save(data interface{}) error {
return sdb.Save(data)
}
func Update(data interface{}) error {
return sdb.Update(data)
}
func UpdateField(data interface{}, fieldName string, value interface{}) error {
return sdb.UpdateField(data, fieldName, value)
}
func Del(data interface{}) error {
return sdb.DeleteStruct(data)
}
func Set(bucket, key string, data interface{}) error {
return sdb.Set(bucket, key, data)
}
func Get(bucket, key string, data interface{}) error {
return sdb.Get(bucket, key, data)
}
func CountAll(data interface{}) int {
n, _ := sdb.Count(data)
return n
}
func One(fieldName string, value interface{}, to interface{}) error {
return sdb.One(fieldName, value, to)
}
func Find(fieldName string, value interface{}, to interface{}, options ...func(q *index.Options)) error {
return sdb.Find(fieldName, value, to, options...)
}
func All(to interface{}, limit, page int) error {
opt := getOpt(limit, page)
return sdb.All(to, opt)
}
func Prefix(fieldName string, prefix string, to interface{}, limit, page int) error {
opt := getOpt(limit, page)
return sdb.Prefix(fieldName, prefix, to, opt)
}
func getOpt(limit, page int) func(*index.Options) {
skip := (page - 1) * limit
opt := func(opt *index.Options) {
opt.Reverse = true
if limit > 0 {
opt.Limit = limit
}
if skip > 0 {
opt.Skip = skip
}
}
return opt
}

33
server/dbdata/db_test.go Normal file
View File

@@ -0,0 +1,33 @@
package dbdata
import (
"os"
"path"
"testing"
"github.com/bjdgyc/anylink/base"
"github.com/stretchr/testify/assert"
)
func preIpData() {
tmpDb := path.Join(os.TempDir(), "anylink_test.db")
base.Cfg.DbFile = tmpDb
initDb()
}
func closeIpdata() {
sdb.Close()
tmpDb := path.Join(os.TempDir(), "anylink_test.db")
os.Remove(tmpDb)
}
func TestDb(t *testing.T) {
assert := assert.New(t)
preIpData()
defer closeIpdata()
u := User{Username: "a"}
_ = Save(&u)
assert.Equal(u.Id, 1)
}

134
server/dbdata/group.go Normal file
View File

@@ -0,0 +1,134 @@
package dbdata
import (
"errors"
"fmt"
"net"
"time"
"github.com/bjdgyc/anylink/base"
)
const (
Allow = "allow"
Deny = "deny"
)
type GroupLinkAcl struct {
// 自上而下匹配 默认 allow * *
Action string `json:"action"` // allow、deny
Val string `json:"val"`
Port uint16 `json:"port"`
IpNet *net.IPNet `json:"ip_net"`
Note string `json:"note"`
}
type ValData struct {
Val string `json:"val"`
IpMask string `json:"ip_mask"`
Note string `json:"note"`
}
type Group struct {
Id int `json:"id" storm:"id,increment"`
Name string `json:"name" storm:"unique"`
Note string `json:"note"`
AllowLan bool `json:"allow_lan"`
ClientDns []ValData `json:"client_dns"`
RouteInclude []ValData `json:"route_include"`
RouteExclude []ValData `json:"route_exclude"`
LinkAcl []GroupLinkAcl `json:"link_acl"`
Bandwidth int `json:"bandwidth"` // 带宽限制
Status int8 `json:"status"` // 1正常
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func GetGroupNames() []string {
var datas []Group
err := All(&datas, 0, 0)
if err != nil {
base.Error(err)
return nil
}
var names []string
for _, v := range datas {
names = append(names, v.Name)
}
return names
}
func SetGroup(g *Group) error {
var err error
if g.Name == "" {
return errors.New("用户组名错误")
}
// 判断数据
clientDns := []ValData{}
for _, v := range g.ClientDns {
if v.Val != "" {
clientDns = append(clientDns, v)
}
}
if len(clientDns) == 0 {
return errors.New("DNS 错误")
}
g.ClientDns = clientDns
routeInclude := []ValData{}
for _, v := range g.RouteInclude {
if v.Val != "" {
ipMask, _, err := parseIpNet(v.Val)
if err != nil {
return errors.New("RouteInclude 错误" + err.Error())
}
v.IpMask = ipMask
routeInclude = append(routeInclude, v)
}
}
g.RouteInclude = routeInclude
routeExclude := []ValData{}
for _, v := range g.RouteExclude {
if v.Val != "" {
ipMask, _, err := parseIpNet(v.Val)
if err != nil {
return errors.New("RouteExclude 错误" + err.Error())
}
v.IpMask = ipMask
routeExclude = append(routeExclude, v)
}
}
g.RouteExclude = routeExclude
// 转换数据
linkAcl := []GroupLinkAcl{}
for _, v := range g.LinkAcl {
if v.Val != "" {
_, ipNet, err := parseIpNet(v.Val)
if err != nil {
return errors.New("GroupLinkAcl 错误" + err.Error())
}
v.IpNet = ipNet
linkAcl = append(linkAcl, v)
}
}
g.LinkAcl = linkAcl
g.UpdatedAt = time.Now()
err = Save(g)
return err
}
func parseIpNet(s string) (string, *net.IPNet, error) {
ip, ipNet, err := net.ParseCIDR(s)
if err != nil {
return "", nil, err
}
mask := net.IP(ipNet.Mask)
ipMask := fmt.Sprintf("%s/%s", ip, mask)
return ipMask, ipNet, nil
}

18
server/dbdata/ip_map.go Normal file
View File

@@ -0,0 +1,18 @@
package dbdata
import (
"net"
"time"
)
type IpMap struct {
Id int `json:"id" storm:"id,increment"`
IpAddr net.IP `json:"ip_addr" storm:"unique"`
MacAddr string `json:"mac_addr" storm:"unique"`
Username string `json:"username"`
Keep bool `json:"keep"` // 保留 ip-mac 绑定
KeepTime time.Time `json:"keep_time"`
Note string `json:"note"` // 备注
LastLogin time.Time `json:"last_login"`
UpdatedAt time.Time `json:"updated_at"`
}

47
server/dbdata/setting.go Normal file
View File

@@ -0,0 +1,47 @@
package dbdata
import (
"reflect"
)
const (
SettingBucket = "SettingBucket"
Installed = "Installed"
)
func StructName(data interface{}) string {
ref := reflect.ValueOf(data)
s := &ref
if s.Kind() == reflect.Ptr {
e := s.Elem()
s = &e
}
name := s.Type().Name()
return name
}
func SettingSet(data interface{}) error {
key := StructName(data)
err := Set(SettingBucket, key, data)
return err
}
func SettingGet(data interface{}) error {
key := StructName(data)
err := Get(SettingBucket, key, data)
return err
}
type SettingSmtp struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
From string `json:"from"`
UseSSl bool `json:"use_ssl"`
}
type SettingOther struct {
Banner string `json:"banner"`
AccountMail string `json:"account_mail"`
}

10
server/dbdata/start.go Normal file
View File

@@ -0,0 +1,10 @@
package dbdata
func Start() {
initDb()
initData()
}
func Stop() error {
return sdb.Close()
}

146
server/dbdata/user.go Normal file
View File

@@ -0,0 +1,146 @@
package dbdata
import (
"errors"
"fmt"
"sync"
"time"
"github.com/bjdgyc/anylink/pkg/utils"
"github.com/xlzd/gotp"
)
type User struct {
Id int `json:"id" storm:"id,increment"`
Username string `json:"username" storm:"unique"`
Nickname string `json:"nickname"`
Email string `json:"email"`
// Password string `json:"password"`
PinCode string `json:"pin_code"`
OtpSecret string `json:"otp_secret"`
DisableOtp bool `json:"disable_otp"` // 禁用otp
Groups []string `json:"groups"`
Status int8 `json:"status"` // 1正常
SendEmail bool `json:"send_email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func SetUser(v *User) error {
var err error
if v.Username == "" || len(v.Groups) == 0 {
return errors.New("用户名或组错误")
}
planPass := v.PinCode
// 自动生成密码
if len(planPass) < 6 {
planPass = utils.RandomNum(8)
}
v.PinCode = planPass
if v.OtpSecret == "" {
v.OtpSecret = gotp.RandomSecret(32)
}
// 判断组是否有效
ng := []string{}
groups := GetGroupNames()
for _, g := range v.Groups {
if utils.InArrStr(groups, g) {
ng = append(ng, g)
}
}
if len(ng) == 0 {
return errors.New("用户名或组错误")
}
v.Groups = ng
v.UpdatedAt = time.Now()
err = Save(v)
return err
}
// 验证用户登陆信息
func CheckUser(name, pwd, group string) error {
// return nil
pl := len(pwd)
if name == "" || pl < 6 {
return fmt.Errorf("%s %s", name, "密码错误")
}
v := &User{}
err := One("Username", name, v)
if err != nil || v.Status != 1 {
return fmt.Errorf("%s %s", name, "用户名错误")
}
// 判断用户组信息
if !utils.InArrStr(v.Groups, group) {
return fmt.Errorf("%s %s", name, "用户组错误")
}
groupData := &Group{}
err = One("Name", group, groupData)
if err != nil || groupData.Status != 1 {
return fmt.Errorf("%s %s", name, "用户组错误")
}
// 判断otp信息
if !v.DisableOtp {
pwd = pwd[:pl-6]
otp := pwd[pl-6:]
if !checkOtp(name, otp, v.OtpSecret) {
return fmt.Errorf("%s %s", name, "动态码错误")
}
}
// 判断用户密码
if pwd != v.PinCode {
return fmt.Errorf("%s %s", name, "密码错误")
}
return nil
}
var (
userOtpMux = sync.Mutex{}
userOtp = map[string]time.Time{}
)
func init() {
go func() {
expire := time.Second * 60
for range time.Tick(time.Second * 10) {
tnow := time.Now()
userOtpMux.Lock()
for k, v := range userOtp {
if tnow.After(v.Add(expire)) {
delete(userOtp, k)
}
}
userOtpMux.Unlock()
}
}()
}
// 判断令牌信息
func checkOtp(name, otp, secret string) bool {
key := fmt.Sprintf("%s:%s", name, otp)
userOtpMux.Lock()
defer userOtpMux.Unlock()
// 令牌只能使用一次
if _, ok := userOtp[key]; ok {
// 已经存在
return false
}
userOtp[key] = time.Now()
totp := gotp.NewDefaultTOTP(secret)
unix := time.Now().Unix()
verify := totp.Verify(otp, int(unix))
return verify
}

5
server/files/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# Binaries for programs and plugins
*
!.gitignore
!index.html

0
server/files/index.html Normal file
View File

24
server/go.mod Normal file
View File

@@ -0,0 +1,24 @@
module github.com/bjdgyc/anylink
go 1.15
require (
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/asdine/storm/v3 v3.2.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/google/gopacket v1.1.19
github.com/gorilla/mux v1.8.0
github.com/pelletier/go-toml v1.8.1
github.com/shirou/gopsutil v3.21.1+incompatible
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
github.com/stretchr/testify v1.7.0
github.com/xhit/go-simple-mail/v2 v2.8.0
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119
go.etcd.io/bbolt v1.3.5
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
)

91
server/go.sum Normal file
View File

@@ -0,0 +1,91 @@
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shirou/gopsutil v3.21.1+incompatible h1:2LwXWdbjXwyDgq26Yy/OT4xozlpmssQfy/rtfhWb0bY=
github.com/shirou/gopsutil v3.21.1+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 h1:1zN6ImoqhSJhN8hGXFaJlSC8msLmIbX8bFqOfWLKw0w=
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091/go.mod h1:N20Z5Y8oye9a7HmytmZ+tr8Q2vlP0tAHP13kTHzwvQY=
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=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xhit/go-simple-mail/v2 v2.8.0 h1:w6ZDXvRk0EO+r78LRlQl14ngP2tiRDRRHhr9UaVJ0p4=
github.com/xhit/go-simple-mail/v2 v2.8.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4=
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119 h1:YyPWX3jLOtYKulBR6AScGIs74lLrJcgeKRwcbAuQOG4=
github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119/go.mod h1:/nuTSlK+okRfR/vnIPqR89fFKonnWPiZymN5ydRJkX8=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU=
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

6
server/handler/dtls.go Normal file
View File

@@ -0,0 +1,6 @@
package handler
// 暂时没有实现
func startDtls() {
}

178
server/handler/link_auth.go Normal file
View File

@@ -0,0 +1,178 @@
package handler
import (
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"text/template"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata"
"github.com/bjdgyc/anylink/sessdata"
)
func LinkAuth(w http.ResponseWriter, r *http.Request) {
// 判断anyconnect客户端
userAgent := strings.ToLower(r.UserAgent())
xAggregateAuth := r.Header.Get("X-Aggregate-Auth")
xTranscendVersion := r.Header.Get("X-Transcend-Version")
if !(strings.Contains(userAgent, "anyconnect") &&
xAggregateAuth == "1" && xTranscendVersion == "1") {
w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, "error request")
return
}
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 != "" {
sessdata.DelSessByStoken(cr.SessionToken)
}
w.WriteHeader(http.StatusOK)
return
}
if cr.Type == "init" {
w.WriteHeader(http.StatusOK)
data := RequestData{Group: cr.GroupSelect, Groups: dbdata.GetGroupNames()}
tplRequest(tpl_request, w, data)
return
}
// 登陆参数判断
if cr.Type != "auth-reply" {
w.WriteHeader(http.StatusBadRequest)
return
}
// TODO 用户密码校验
err = dbdata.CheckUser(cr.Auth.Username, cr.Auth.Password, cr.GroupSelect)
if err != nil {
base.Warn(err)
w.WriteHeader(http.StatusOK)
data := RequestData{Group: cr.GroupSelect, Groups: dbdata.GetGroupNames(), Error: "用户名或密码错误"}
tplRequest(tpl_request, w, data)
return
}
// if !ok {
// w.WriteHeader(http.StatusOK)
// data := RequestData{Group: cr.GroupSelect, Groups: base.Cfg.UserGroups, Error: "请先激活用户"}
// tplRequest(tpl_request, w, data)
// return
// }
// 创建新的session信息
sess := sessdata.NewSession("")
sess.Username = cr.Auth.Username
sess.Group = cr.GroupSelect
sess.MacAddr = strings.ToLower(cr.MacAddressList.MacAddress)
sess.UniqueIdGlobal = cr.DeviceId.UniqueIdGlobal
other := &dbdata.SettingOther{}
_ = dbdata.SettingGet(other)
rd := RequestData{SessionId: sess.Sid, SessionToken: sess.Sid + "@" + sess.Token,
Banner: other.Banner}
w.WriteHeader(http.StatusOK)
tplRequest(tpl_complete, w, rd)
base.Debug("login", cr.Auth.Username)
}
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", "&#x0A;")
}
t, _ := template.New("auth_complete").Parse(auth_complete)
_ = t.Execute(w, data)
}
// 设置输出信息
type RequestData struct {
Groups []string
Group string
Error string
// complete
SessionId string
SessionToken string
Banner string
}
var auth_request = `<?xml version="1.0" encoding="UTF-8"?>
<config-auth client="vpn" type="auth-request" aggregate-auth-version="2">
<opaque is-for="sg">
<tunnel-group>{{.Group}}</tunnel-group>
<group-alias>{{.Group}}</group-alias>
<aggauth-handle>168179266</aggauth-handle>
<config-hash>1595829378234</config-hash>
<auth-method>multiple-cert</auth-method>
<auth-method>single-sign-on-v2</auth-method>
</opaque>
<auth id="main">
<title>Login</title>
<message>请输入你的用户名和密码</message>
<banner></banner>
{{if .Error}}
<error id="88" param1="{{.Error}}" param2="">登陆失败: %s</error>
{{end}}
<form>
<input type="text" name="username" label="Username:"></input>
<input type="password" name="password" label="Password:"></input>
<select name="group_list" label="GROUP:">
{{range $v := .Groups}}
<option {{if eq $v $.Group}} selected="true"{{end}}>{{$v}}</option>
{{end}}
</select>
</form>
</auth>
</config-auth>
`
var auth_complete = `<?xml version="1.0" encoding="UTF-8"?>
<config-auth client="vpn" type="complete" aggregate-auth-version="2">
<session-id>{{.SessionId}}</session-id>
<session-token>{{.SessionToken}}</session-token>
<auth id="success">
<banner>{{.Banner}}</banner>
<message id="0" param1="" param2=""></message>
</auth>
<capabilities>
<crypto-supported>ssl-dhe</crypto-supported>
</capabilities>
<config client="vpn" type="private">
<vpn-base-config>
<server-cert-hash>240B97A685B2BFA66AD699B90AAC49EA66495D69</server-cert-hash>
</vpn-base-config>
<opaque is-for="vpn-client"></opaque>
</config>
</config-auth>
`

View File

@@ -0,0 +1,66 @@
package handler
import (
"encoding/xml"
"log"
"net/http"
"os/exec"
)
const BufferSize = 2048
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"`
}
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")
}
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
}

115
server/handler/link_cstp.go Normal file
View File

@@ -0,0 +1,115 @@
package handler
import (
"encoding/binary"
"net"
"time"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/sessdata"
)
func LinkCstp(conn net.Conn, cSess *sessdata.ConnSession) {
defer func() {
base.Debug("LinkCstp return", cSess.IpAddr)
_ = conn.Close()
cSess.Close()
}()
var (
err error
n int
dataLen uint16
dead = time.Duration(cSess.CstpDpd+5) * time.Second
)
go cstpWrite(conn, cSess)
for {
// 设置超时限制
err = conn.SetReadDeadline(time.Now().Add(dead))
if err != nil {
base.Error("SetDeadline: ", err)
return
}
hdata := make([]byte, BufferSize)
n, err = conn.Read(hdata)
if err != nil {
base.Error("read hdata: ", err)
return
}
// 限流设置
err = cSess.RateLimit(n, true)
if err != nil {
base.Error(err)
}
switch hdata[6] {
case 0x07: // KEEPALIVE
// do nothing
// base.Debug("recv keepalive", cSess.IpAddr)
case 0x05: // DISCONNECT
base.Debug("DISCONNECT", cSess.IpAddr)
return
case 0x03: // DPD-REQ
// base.Debug("recv DPD-REQ", cSess.IpAddr)
if payloadOut(cSess, sessdata.LTypeIPData, 0x04, nil) {
return
}
case 0x04:
// log.Println("recv DPD-RESP")
case 0x00: // DATA
dataLen = binary.BigEndian.Uint16(hdata[4:6]) // 4,5
if payloadIn(cSess, sessdata.LTypeIPData, 0x00, hdata[8:8+dataLen]) {
return
}
}
}
}
func cstpWrite(conn net.Conn, cSess *sessdata.ConnSession) {
defer func() {
base.Debug("cstpWrite return", cSess.IpAddr)
_ = conn.Close()
cSess.Close()
}()
var (
err error
n int
header []byte
payload *sessdata.Payload
)
for {
select {
case payload = <-cSess.PayloadOut:
case <-cSess.CloseChan:
return
}
if payload.LType != sessdata.LTypeIPData {
continue
}
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...)
}
n, err = conn.Write(header)
if err != nil {
base.Error("write err", err)
return
}
// 限流设置
err = cSess.RateLimit(n, false)
if err != nil {
base.Error(err)
}
}
}

View File

@@ -0,0 +1,39 @@
package handler
import (
"fmt"
"net/http"
"strings"
"github.com/bjdgyc/anylink/admin"
)
func LinkHome(w http.ResponseWriter, r *http.Request) {
// fmt.Println(r.RemoteAddr)
// hu, _ := httputil.DumpRequest(r, true)
// fmt.Println("DumpHome: ", string(hu))
connection := strings.ToLower(r.Header.Get("Connection"))
userAgent := strings.ToLower(r.UserAgent())
if connection == "close" && strings.Contains(userAgent, "anyconnect") {
w.Header().Set("Connection", "close")
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "hello world")
}
func LinkOtpQr(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
idS := r.FormValue("id")
jwtToken := r.FormValue("jwt")
data, err := admin.GetJwtData(jwtToken)
if err != nil || idS != fmt.Sprint(data["id"]) {
w.WriteHeader(http.StatusForbidden)
return
}
admin.UserOtpQr(w, r)
}

241
server/handler/link_tap.go Normal file
View File

@@ -0,0 +1,241 @@
package handler
import (
"fmt"
"net"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/pkg/arpdis"
"github.com/bjdgyc/anylink/sessdata"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/songgao/packets/ethernet"
"github.com/songgao/water"
"github.com/songgao/water/waterutil"
)
const bridgeName = "anylink0"
var (
bridgeIp net.IP
bridgeHw net.HardwareAddr
)
func checkTap() {
brFace, err := net.InterfaceByName(bridgeName)
if err != nil {
base.Fatal("testTap err: ", err)
}
bridgeHw = brFace.HardwareAddr
addrs, err := brFace.Addrs()
if err != nil {
base.Fatal("testTap err: ", err)
}
for _, addr := range addrs {
ip, _, err := net.ParseCIDR(addr.String())
if err != nil || ip.To4() == nil {
continue
}
bridgeIp = ip
}
if bridgeIp == nil && bridgeHw == nil {
base.Fatal("bridgeIp is err")
}
if !sessdata.IpPool.Ipv4IPNet.Contains(bridgeIp) {
base.Fatal("bridgeIp or Ip network err")
}
}
// 创建tap网卡
func LinkTap(cSess *sessdata.ConnSession) error {
cfg := water.Config{
DeviceType: water.TAP,
}
ifce, err := water.New(cfg)
if err != nil {
base.Error(err)
return err
}
cSess.TunName = ifce.Name()
// arp on
cmdstr1 := fmt.Sprintf("ip link set dev %s up mtu %d multicast on", ifce.Name(), cSess.Mtu)
cmdstr2 := fmt.Sprintf("sysctl -w net.ipv6.conf.%s.disable_ipv6=1", ifce.Name())
cmdstr3 := fmt.Sprintf("ip link set dev %s master %s", ifce.Name(), bridgeName)
cmdStrs := []string{cmdstr1, cmdstr2, cmdstr3}
err = execCmd(cmdStrs)
if err != nil {
base.Error(err)
_ = ifce.Close()
return err
}
go tapRead(ifce, cSess)
go tapWrite(ifce, cSess)
return nil
}
func tapWrite(ifce *water.Interface, cSess *sessdata.ConnSession) {
defer func() {
base.Debug("LinkTap return", cSess.IpAddr)
cSess.Close()
_ = ifce.Close()
}()
var (
err error
payload *sessdata.Payload
)
for {
select {
case payload = <-cSess.PayloadIn:
case <-cSess.CloseChan:
return
}
var frame ethernet.Frame
switch payload.LType {
default:
// log.Println(payload)
case sessdata.LTypeEthernet:
frame = payload.Data
case sessdata.LTypeIPData: // 需要转换成 Ethernet 数据
data := payload.Data
ip_src := waterutil.IPv4Source(data)
if waterutil.IsIPv6(data) || !ip_src.Equal(cSess.IpAddr) {
// 过滤掉IPv6的数据
// 非分配给客户端ip直接丢弃
continue
}
// packet := gopacket.NewPacket(data, layers.LayerTypeIPv4, gopacket.Default)
// fmt.Println("get:", packet)
ip_dst := waterutil.IPv4Destination(data)
// fmt.Println("get:", ip_src, ip_dst)
var dstHw net.HardwareAddr
if !sessdata.IpPool.Ipv4IPNet.Contains(ip_dst) || ip_dst.Equal(sessdata.IpPool.Ipv4Gateway) {
// 不是同一网段使用网关mac地址
dstAddr := arpdis.Lookup(sessdata.IpPool.Ipv4Gateway, false)
dstHw = dstAddr.HardwareAddr
} else {
dstAddr := arpdis.Lookup(ip_dst, true)
// fmt.Println("dstAddr", dstAddr)
if dstAddr != nil {
dstHw = dstAddr.HardwareAddr
} else {
dstHw = bridgeHw
}
}
// fmt.Println("Gateway", ip_dst, dstAddr.HardwareAddr)
frame.Prepare(dstHw, cSess.MacHw, ethernet.NotTagged, ethernet.IPv4, len(data))
copy(frame[12+2:], data)
}
// packet := gopacket.NewPacket(frame, layers.LayerTypeEthernet, gopacket.Default)
// fmt.Println("write:", packet)
_, err = ifce.Write(frame)
if err != nil {
base.Error("tap Write err", err)
return
}
}
}
func tapRead(ifce *water.Interface, cSess *sessdata.ConnSession) {
defer func() {
base.Debug("tapRead return", cSess.IpAddr)
_ = ifce.Close()
}()
var (
err error
n int
buf []byte
)
for {
var frame ethernet.Frame
frame.Resize(BufferSize)
n, err = ifce.Read(frame)
if err != nil {
base.Error("tap Read err", n, err)
return
}
frame = frame[:n]
switch frame.Ethertype() {
default:
// packet := gopacket.NewPacket(frame, layers.LayerTypeEthernet, gopacket.Default)
// fmt.Println(packet)
continue
case ethernet.IPv6:
continue
case ethernet.IPv4:
// 发送IP数据
data := frame.Payload()
ip_dst := waterutil.IPv4Destination(data)
if !ip_dst.Equal(cSess.IpAddr) {
// 过滤非本机地址
// log.Println(ip_dst, sess.Ip)
continue
}
// packet := gopacket.NewPacket(data, layers.LayerTypeIPv4, gopacket.Default)
// fmt.Println("put:", packet)
if payloadOut(cSess, sessdata.LTypeIPData, 0x00, data) {
return
}
case ethernet.ARP:
// 暂时仅实现了ARP协议
packet := gopacket.NewPacket(frame, layers.LayerTypeEthernet, gopacket.Default)
layer := packet.Layer(layers.LayerTypeARP)
arpReq := layer.(*layers.ARP)
if !cSess.IpAddr.Equal(arpReq.DstProtAddress) {
// 过滤非本机地址
continue
}
// fmt.Println("arp", net.IP(arpReq.SourceProtAddress), sess.Ip)
// fmt.Println(packet)
// 返回ARP数据
src := &arpdis.Addr{IP: cSess.IpAddr, HardwareAddr: cSess.MacHw}
dst := &arpdis.Addr{IP: arpReq.SourceProtAddress, HardwareAddr: frame.Source()}
buf, err = arpdis.NewARPReply(src, dst)
if err != nil {
base.Error(err)
return
}
// 从接受的arp信息添加arp地址
addr := &arpdis.Addr{
IP: make([]byte, len(arpReq.SourceProtAddress)),
HardwareAddr: make([]byte, len(frame.Source())),
}
// addr.IP = arpReq.SourceProtAddress
// addr.HardwareAddr = frame.Source()
copy(addr.IP, arpReq.SourceProtAddress)
copy(addr.HardwareAddr, frame.Source())
arpdis.Add(addr)
if payloadIn(cSess, sessdata.LTypeEthernet, 0x00, buf) {
return
}
}
}
}

122
server/handler/link_tun.go Normal file
View File

@@ -0,0 +1,122 @@
package handler
import (
"fmt"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/sessdata"
"github.com/songgao/water"
)
func checkTun() {
// 测试tun
cfg := water.Config{
DeviceType: water.TUN,
}
ifce, err := water.New(cfg)
if err != nil {
base.Fatal("open tun err: ", err)
}
defer ifce.Close()
// 测试ip命令
cmdstr := fmt.Sprintf("ip link set dev %s up mtu %s multicast off", ifce.Name(), "1399")
err = execCmd([]string{cmdstr})
if err != nil {
base.Fatal("testTun err: ", err)
}
}
// 创建tun网卡
func LinkTun(cSess *sessdata.ConnSession) error {
cfg := water.Config{
DeviceType: water.TUN,
}
ifce, err := water.New(cfg)
if err != nil {
base.Error(err)
return err
}
// log.Printf("Interface Name: %s\n", ifce.Name())
cSess.SetTunName(ifce.Name())
// cSess.TunName = ifce.Name()
cmdstr1 := fmt.Sprintf("ip link set dev %s up mtu %d multicast off", ifce.Name(), cSess.Mtu)
cmdstr2 := fmt.Sprintf("ip addr add dev %s local %s peer %s/32",
ifce.Name(), base.Cfg.Ipv4Gateway, cSess.IpAddr)
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 {
base.Error(err)
_ = ifce.Close()
return err
}
go tunRead(ifce, cSess)
go tunWrite(ifce, cSess)
return nil
}
func tunWrite(ifce *water.Interface, cSess *sessdata.ConnSession) {
defer func() {
base.Debug("LinkTun return", cSess.IpAddr)
cSess.Close()
_ = ifce.Close()
}()
var (
err error
payload *sessdata.Payload
)
for {
select {
case payload = <-cSess.PayloadIn:
case <-cSess.CloseChan:
return
}
_, err = ifce.Write(payload.Data)
if err != nil {
base.Error("tun Write err", err)
return
}
}
}
func tunRead(ifce *water.Interface, cSess *sessdata.ConnSession) {
defer func() {
base.Debug("tunRead return", cSess.IpAddr)
_ = ifce.Close()
}()
var (
err error
n int
)
for {
data := make([]byte, BufferSize)
n, err = ifce.Read(data)
if err != nil {
base.Error("tun Read err", n, err)
return
}
data = data[:n]
// ip_src := waterutil.IPv4Source(data)
// ip_dst := waterutil.IPv4Destination(data)
// ip_port := waterutil.IPv4DestinationPort(data)
// fmt.Println("sent:", ip_src, ip_dst, ip_port)
// packet := gopacket.NewPacket(data, layers.LayerTypeIPv4, gopacket.Default)
// fmt.Println("read:", packet)
if payloadOut(cSess, sessdata.LTypeIPData, 0x00, data) {
return
}
}
}

View File

@@ -0,0 +1,161 @@
package handler
import (
"bytes"
"fmt"
"log"
"net"
"net/http"
"os"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/sessdata"
)
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("LinkTunnel", r.RemoteAddr)
// 判断session-token的值
cookie, err := r.Cookie("webvpn")
if err != nil || cookie.Value == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
sess := sessdata.SToken2Sess(cookie.Value)
if sess == nil {
w.WriteHeader(http.StatusBadRequest)
return
}
// 开启link
cSess := sess.NewConn()
if cSess == nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
return
}
// 客户端信息
cstpMtu := r.Header.Get("X-CSTP-MTU")
masterSecret := r.Header.Get("X-DTLS-Master-Secret")
localIp := r.Header.Get("X-Cstp-Local-Address-Ip4")
mobile := r.Header.Get("X-Cstp-License")
cSess.SetMtu(cstpMtu)
cSess.MasterSecret = masterSecret
cSess.RemoteAddr = r.RemoteAddr
cSess.LocalIp = net.ParseIP(localIp)
cstpKeepalive := base.Cfg.CstpKeepalive
cstpDpd := base.Cfg.CstpDpd
cSess.Client = "pc"
if mobile == "mobile" {
// 手机客户端
cstpKeepalive = base.Cfg.MobileKeepalive
cstpDpd = base.Cfg.MobileDpd
cSess.Client = "mobile"
}
cSess.CstpDpd = cstpDpd
base.Debug(cSess.IpAddr, cSess.MacHw, sess.Username, mobile)
// 返回客户端数据
w.Header().Set("Server", fmt.Sprintf("%s %s", base.APP_NAME, base.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", cSess.IpAddr.String()) // 分配的ip地址
w.Header().Set("X-CSTP-Netmask", sessdata.IpPool.Ipv4Mask.String()) // 子网掩码
w.Header().Set("X-CSTP-Hostname", hn) // 机器名称
// 允许本地LAN访问vpn网络必须放在路由的第一个
if cSess.Group.AllowLan {
w.Header().Set("X-CSTP-Split-Exclude", "0.0.0.0/255.255.255.255")
}
// dns地址
for _, v := range cSess.Group.ClientDns {
w.Header().Add("X-CSTP-DNS", v.Val)
}
// 允许的路由
for _, v := range cSess.Group.RouteInclude {
w.Header().Add("X-CSTP-Split-Include", v.IpMask)
}
// 不允许的路由
for _, v := range cSess.Group.RouteExclude {
w.Header().Add("X-CSTP-Split-Exclude", v.IpMask)
}
w.Header().Set("X-CSTP-Lease-Duration", fmt.Sprintf("%d", base.Cfg.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", "172800")
w.Header().Set("X-CSTP-Rekey-Method", "new-tunnel")
w.Header().Set("X-CSTP-DPD", fmt.Sprintf("%d", cstpDpd))
w.Header().Set("X-CSTP-Keepalive", fmt.Sprintf("%d", cstpKeepalive))
// w.Header().Set("X-CSTP-Banner", banner.Banner)
w.Header().Set("X-CSTP-MSIE-Proxy-Lockdown", "true")
w.Header().Set("X-CSTP-Smartcard-Removal-Disconnect", "true")
w.Header().Set("X-CSTP-MTU", fmt.Sprintf("%d", cSess.Mtu)) // 1399
w.Header().Set("X-DTLS-MTU", fmt.Sprintf("%d", cSess.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", base.Cfg.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)
hClone := w.Header().Clone()
headers := make([]byte, 0)
buf := bytes.NewBuffer(headers)
_ = hClone.Write(buf)
base.Debug(buf.String())
hj := w.(http.Hijacker)
conn, _, err := hj.Hijack()
if err != nil {
base.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// 开始数据处理
switch base.Cfg.LinkMode {
case base.LinkModeTUN:
err = LinkTun(cSess)
case base.LinkModeTAP:
err = LinkTap(cSess)
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
go LinkCstp(conn, cSess)
}

91
server/handler/payload.go Normal file
View File

@@ -0,0 +1,91 @@
package handler
import (
"github.com/bjdgyc/anylink/dbdata"
"github.com/bjdgyc/anylink/sessdata"
"github.com/songgao/water/waterutil"
)
func payloadIn(cSess *sessdata.ConnSession, lType sessdata.LType, pType byte, data []byte) bool {
payload := &sessdata.Payload{
LType: lType,
PType: pType,
Data: data,
}
return payloadInData(cSess, payload)
}
func payloadInData(cSess *sessdata.ConnSession, payload *sessdata.Payload) bool {
// 进行Acl规则判断
check := checkLinkAcl(cSess.Group, payload)
if !check {
// 校验不通过直接丢弃
return false
}
closed := false
select {
case cSess.PayloadIn <- payload:
case <-cSess.CloseChan:
closed = true
}
return closed
}
func payloadOut(cSess *sessdata.ConnSession, lType sessdata.LType, pType byte, data []byte) bool {
payload := &sessdata.Payload{
LType: lType,
PType: pType,
Data: data,
}
return payloadOutData(cSess, payload)
}
func payloadOutData(cSess *sessdata.ConnSession, payload *sessdata.Payload) bool {
closed := false
select {
case cSess.PayloadOut <- payload:
case <-cSess.CloseChan:
closed = true
}
return closed
}
// Acl规则校验
func checkLinkAcl(group *dbdata.Group, payload *sessdata.Payload) bool {
if payload.LType == sessdata.LTypeIPData && payload.PType == 0x00 && len(group.LinkAcl) > 0 {
} else {
return true
}
ip_dst := waterutil.IPv4Destination(payload.Data)
ip_port := waterutil.IPv4DestinationPort(payload.Data)
// fmt.Println("sent:", ip_dst, ip_port)
// 优先放行dns端口
for _, v := range group.ClientDns {
if v.Val == ip_dst.String() && ip_port == 53 {
return true
}
}
for _, v := range group.LinkAcl {
// 循环判断ip和端口
if v.IpNet.Contains(ip_dst) {
if v.Port == ip_port || v.Port == 0 {
if v.Action == dbdata.Allow {
return true
} else {
return false
}
}
}
}
return false
}

81
server/handler/server.go Normal file
View File

@@ -0,0 +1,81 @@
package handler
import (
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"time"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/pkg/proxyproto"
"github.com/gorilla/mux"
)
func GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(base.Cfg.CertFile, base.Cfg.CertKey)
return &cert, err
}
func startTls() {
addr := base.Cfg.ServerAddr
certFile := base.Cfg.CertFile
keyFile := base.Cfg.CertKey
// 设置tls信息
tlsConfig := &tls.Config{
NextProtos: []string{"http/1.1"},
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true,
GetCertificate: GetCertificate,
}
srv := &http.Server{
Addr: addr,
Handler: initRoute(),
TLSConfig: tlsConfig,
ErrorLog: base.GetBaseLog(),
}
var ln net.Listener
ln, err := net.Listen("tcp", addr)
if err != nil {
log.Fatal(err)
}
defer ln.Close()
if base.Cfg.ProxyProtocol {
ln = &proxyproto.Listener{Listener: ln, ProxyHeaderTimeout: time.Second * 5}
}
base.Info("listen server", addr)
err = srv.ServeTLS(ln, certFile, keyFile)
if err != nil {
base.Fatal(err)
}
}
func initRoute() http.Handler {
r := mux.NewRouter()
r.HandleFunc("/", LinkHome).Methods(http.MethodGet)
r.HandleFunc("/", LinkAuth).Methods(http.MethodPost)
r.HandleFunc("/CSCOSSLC/tunnel", LinkTunnel).Methods(http.MethodConnect)
r.HandleFunc("/otp_qr", LinkOtpQr).Methods(http.MethodGet)
r.PathPrefix("/files/").Handler(
http.StripPrefix("/files/",
http.FileServer(http.Dir(base.Cfg.FilesPath)),
),
)
r.NotFoundHandler = http.HandlerFunc(notFound)
return r
}
func notFound(w http.ResponseWriter, r *http.Request) {
// fmt.Println(r.RemoteAddr)
// hu, _ := httputil.DumpRequest(r, true)
// fmt.Println("NotFound: ", string(hu))
w.WriteHeader(http.StatusNotFound)
fmt.Fprintln(w, "404 page not found")
}

25
server/handler/start.go Normal file
View File

@@ -0,0 +1,25 @@
package handler
import (
"github.com/bjdgyc/anylink/admin"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata"
"github.com/bjdgyc/anylink/sessdata"
)
func Start() {
dbdata.Start()
sessdata.Start()
checkTun()
if base.Cfg.LinkMode == base.LinkModeTAP {
checkTap()
}
go admin.StartAdmin()
go startTls()
go startDtls()
}
func Stop() {
_ = dbdata.Stop()
}

47
server/main.go Normal file
View File

@@ -0,0 +1,47 @@
// AnyLink 是一个企业级远程办公vpn软件可以支持多人同时在线使用。
// +build linux
package main
import (
"os"
"os/signal"
"syscall"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/handler"
)
// 程序版本
var COMMIT_ID string
func main() {
base.CommitId = COMMIT_ID
base.Start()
handler.Start()
signalWatch()
}
func signalWatch() {
base.Info("Server pid: ", os.Getpid())
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGALRM)
for {
sig := <-sigs
base.Info("Get signal:", sig)
switch sig {
case syscall.SIGUSR2:
// reload
base.Info("Reload")
default:
// stop
base.Info("Stop")
handler.Stop()
return
}
}
}

99
server/pkg/arpdis/addr.go Normal file
View File

@@ -0,0 +1,99 @@
package arpdis
import (
"net"
"sync"
"time"
)
const (
StaleTimeNormal = time.Minute * 5
StaleTimeUnreachable = time.Minute * 10
TypeNormal = 0
TypeUnreachable = 1
TypeStatic = 2
)
var (
table = make(map[string]*Addr)
tableMu sync.RWMutex
)
type Addr struct {
IP net.IP
HardwareAddr net.HardwareAddr
disTime time.Time
Type int8
}
func Lookup(ip net.IP, onlyTable bool) *Addr {
addr := tableLookup(ip.To4())
if addr != nil || onlyTable {
return addr
}
addr = doLookup(ip.To4())
Add(addr)
return addr
}
// Add adds a IP-MAC map to a runtime ARP table.
func tableLookup(ip net.IP) *Addr {
tableMu.Lock()
addr := table[ip.To4().String()]
tableMu.Unlock()
if addr == nil {
return nil
}
// 判断老化过期时间
tsub := time.Since(addr.disTime)
switch addr.Type {
case TypeNormal:
if tsub > StaleTimeNormal {
return nil
}
case TypeUnreachable:
if tsub > StaleTimeUnreachable {
return nil
}
case TypeStatic:
}
return addr
}
// Add adds a IP-MAC map to a runtime ARP table.
func Add(addr *Addr) {
if addr == nil {
return
}
if addr.disTime.IsZero() {
addr.disTime = time.Now()
}
ip := addr.IP.To4().String()
tableMu.Lock()
defer tableMu.Unlock()
if a, ok := table[ip]; ok {
// 静态地址只能设置一次
if a.Type == TypeStatic {
return
}
}
table[ip] = addr
}
// Delete removes an IP from the runtime ARP table.
func Delete(ip net.IP) {
tableMu.Lock()
defer tableMu.Unlock()
delete(table, ip.To4().String())
}
// List returns the current runtime ARP table.
func List() map[string]*Addr {
tableMu.RLock()
defer tableMu.RUnlock()
return table
}

View File

@@ -0,0 +1,35 @@
package arpdis
import (
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestLookup(t *testing.T) {
assert := assert.New(t)
ip := net.IPv4(192, 168, 10, 2)
hw, _ := net.ParseMAC("08:00:27:a0:17:42")
now := time.Now()
addr1 := &Addr{
IP: ip,
HardwareAddr: hw,
Type: TypeStatic,
disTime: now,
}
Add(addr1)
addr2 := Lookup(ip, true)
assert.Equal(addr1, addr2)
addr3 := &Addr{
IP: ip,
HardwareAddr: hw,
Type: TypeNormal,
disTime: now,
}
Add(addr3)
addr4 := Lookup(ip, true)
// 静态地址只能设置一次
assert.NotEqual(addr3, addr4)
}

56
server/pkg/arpdis/arp.go Normal file
View File

@@ -0,0 +1,56 @@
package arpdis
// Reference: github.com/malfunkt/arpfox
// TODO now only support IPv4
import (
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
var defaultSerializeOpts = gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}
// NewARPRequest creates a bew ARP packet of type "request.
func NewARPRequest(src *Addr, dst *Addr) ([]byte, error) {
return buildPacket(src, dst, layers.ARPRequest)
}
// NewARPReply creates a new ARP packet of type "reply".
func NewARPReply(src *Addr, dst *Addr) ([]byte, error) {
return buildPacket(src, dst, layers.ARPReply)
}
// buildPacket creates an template ARP packet with the given source and
// destination.
func buildPacket(src *Addr, dst *Addr, opt uint16) ([]byte, error) {
ether := layers.Ethernet{
EthernetType: layers.EthernetTypeARP,
SrcMAC: src.HardwareAddr,
DstMAC: dst.HardwareAddr,
}
arp := layers.ARP{
AddrType: layers.LinkTypeEthernet,
Protocol: layers.EthernetTypeIPv4,
HwAddressSize: 6,
ProtAddressSize: 4,
Operation: opt,
SourceHwAddress: src.HardwareAddr,
SourceProtAddress: src.IP.To4(),
DstHwAddress: dst.HardwareAddr,
DstProtAddress: dst.IP.To4(),
}
buf := gopacket.NewSerializeBuffer()
err := gopacket.SerializeLayers(buf, defaultSerializeOpts, &ether, &arp)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}

101
server/pkg/arpdis/icmp.go Normal file
View File

@@ -0,0 +1,101 @@
package arpdis
import (
"errors"
"net"
"os"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
const (
ProtocolICMP = 1
ProtocolIPv6ICMP = 58
)
func doPing(ip string) error {
raddr, _ := net.ResolveIPAddr("ip4:icmp", ip)
conn, err := icmp.ListenPacket("ip4:icmp", "")
if err != nil {
return err
}
ipv4Conn := conn.IPv4PacketConn()
// 限制跳跃数
err = ipv4Conn.SetTTL(10)
if err != nil {
return err
}
msg := &icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Seq: 1,
Data: timeToBytes(time.Now()),
},
}
b, err := msg.Marshal(nil)
if err != nil {
return err
}
_, err = conn.WriteTo(b, raddr)
if err != nil {
return err
}
_ = conn.SetReadDeadline(time.Now().Add(time.Second * 2))
for {
buf := make([]byte, 512)
n, dst, err := conn.ReadFrom(buf)
if err != nil {
return err
}
if dst.String() != ip {
continue
}
var result *icmp.Message
result, err = icmp.ParseMessage(ProtocolICMP, buf[:n])
if err != nil {
return err
}
switch result.Type {
case ipv4.ICMPTypeEchoReply:
// success
if rply, ok := result.Body.(*icmp.Echo); ok {
_ = rply
// log.Printf("%+v \n", rply)
}
return nil
// case ipv4.ICMPTypeTimeExceeded:
// case ipv4.ICMPTypeDestinationUnreachable:
default:
return errors.New("DestinationUnreachable")
}
}
}
func timeToBytes(t time.Time) []byte {
nsec := t.UnixNano()
b := make([]byte, 8)
for i := uint8(0); i < 8; i++ {
b[i] = byte((nsec >> ((7 - i) * 8)) & 0xff)
}
return b
}
func bytesToTime(b []byte) time.Time {
var nsec int64
for i := uint8(0); i < 8; i++ {
nsec += int64(b[i]) << ((7 - i) * 8)
}
return time.Unix(nsec/1000000000, nsec%1000000000)
}

View File

@@ -0,0 +1,61 @@
// Currently only Darwin and Linux support this.
package arpdis
import (
"log"
"net"
"os/exec"
"strings"
)
func doLookup(ip net.IP) *Addr {
// ping := exec.Command("ping", "-c1", "-t1", ip.String())
// if err := ping.Run(); err != nil {
// addr := &Addr{IP: ip, Type: TypeUnreachable}
// return addr
// }
err := doPing(ip.String())
if err != nil {
// log.Println(err)
addr := &Addr{IP: ip, Type: TypeUnreachable}
return addr
}
return doArpShow(ip)
}
func doArpShow(ip net.IP) *Addr {
cmd := exec.Command("ip", "n", "show", ip.String())
out, err := cmd.Output()
if err != nil {
log.Println("lookup show", err)
return nil
}
// os.Open("/proc/net/arp")
// 192.168.1.2 0x1 0x2 e0:94:67:e2:42:5d * eth0
// 192.168.1.2 dev eth0 lladdr 08:00:27:94:a5:a4 STALE
outS := strings.ReplaceAll(string(out), " ", " ")
outS = strings.TrimSpace(outS)
arpArr := strings.Split(outS, " ")
if len(arpArr) != 6 {
log.Println("lookup arpArr", outS, ip)
return nil
}
mac, err := net.ParseMAC(arpArr[4])
if err != nil {
log.Println("lookup mac", outS, err)
return nil
}
return &Addr{IP: ip, HardwareAddr: mac}
}
// IP address HW type Flags HW address Mask Device
// 172.23.24.12 0x1 0x2 00:e0:4c:73:5c:48 * anylink0
// 172.23.24.1 0x1 0x2 3c:8c:40:a0:7a:2d * anylink0
// 172.23.24.13 0x1 0x2 00:1c:42:4d:33:46 * anylink0
// 172.23.24.2 0x1 0x0 00:00:00:00:00:00 * anylink0
// 172.23.24.14 0x1 0x0 00:00:00:00:00:00 * anylink0

View File

@@ -0,0 +1,290 @@
// copy from: https://github.com/armon/go-proxyproto/blob/master/protocol.go
// design: http://www.haproxy.org/download/2.2/doc/proxy-protocol.txt
// HAProxy proxy proto v1
package proxyproto
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"log"
"net"
"strconv"
"strings"
"sync"
"time"
)
var (
// prefix is the string we look for at the start of a connection
// to check if this connection is using the proxy protocol
prefix = []byte("PROXY ")
prefixLen = len(prefix)
ErrInvalidUpstream = errors.New("upstream connection address not trusted for PROXY information")
)
// SourceChecker can be used to decide whether to trust the PROXY info or pass
// the original connection address through. If set, the connecting address is
// passed in as an argument. If the function returns an error due to the source
// being disallowed, it should return ErrInvalidUpstream.
//
// If error is not nil, the call to Accept() will fail. If the reason for
// triggering this failure is due to a disallowed source, it should return
// ErrInvalidUpstream.
//
// If bool is true, the PROXY-set address is used.
//
// If bool is false, the connection's remote address is used, rather than the
// address claimed in the PROXY info.
type SourceChecker func(net.Addr) (bool, error)
// Listener is used to wrap an underlying listener,
// whose connections may be using the HAProxy Proxy Protocol (version 1).
// If the connection is using the protocol, the RemoteAddr() will return
// the correct client address.
//
// Optionally define ProxyHeaderTimeout to set a maximum time to
// receive the Proxy Protocol Header. Zero means no timeout.
type Listener struct {
Listener net.Listener
ProxyHeaderTimeout time.Duration
SourceCheck SourceChecker
UnknownOK bool // allow PROXY UNKNOWN
}
// Conn is used to wrap and underlying connection which
// may be speaking the Proxy Protocol. If it is, the RemoteAddr() will
// return the address of the client instead of the proxy address.
type Conn struct {
bufReader *bufio.Reader
conn net.Conn
dstAddr *net.TCPAddr
srcAddr *net.TCPAddr
useConnAddr bool
once sync.Once
proxyHeaderTimeout time.Duration
unknownOK bool
}
// Accept waits for and returns the next connection to the listener.
func (p *Listener) Accept() (net.Conn, error) {
// Get the underlying connection
conn, err := p.Listener.Accept()
if err != nil {
return nil, err
}
var useConnAddr bool
if p.SourceCheck != nil {
allowed, err := p.SourceCheck(conn.RemoteAddr())
if err != nil {
return nil, err
}
if !allowed {
useConnAddr = true
}
}
newConn := NewConn(conn, p.ProxyHeaderTimeout)
newConn.useConnAddr = useConnAddr
newConn.unknownOK = p.UnknownOK
return newConn, nil
}
// Close closes the underlying listener.
func (p *Listener) Close() error {
return p.Listener.Close()
}
// Addr returns the underlying listener's network address.
func (p *Listener) Addr() net.Addr {
return p.Listener.Addr()
}
// NewConn is used to wrap a net.Conn that may be speaking
// the proxy protocol into a proxyproto.Conn
func NewConn(conn net.Conn, timeout time.Duration) *Conn {
pConn := &Conn{
bufReader: bufio.NewReader(conn),
conn: conn,
proxyHeaderTimeout: timeout,
}
return pConn
}
// Read is check for the proxy protocol header when doing
// the initial scan. If there is an error parsing the header,
// it is returned and the socket is closed.
func (p *Conn) Read(b []byte) (int, error) {
var err error
p.once.Do(func() { err = p.checkPrefix() })
if err != nil {
return 0, err
}
return p.bufReader.Read(b)
}
func (p *Conn) ReadFrom(r io.Reader) (int64, error) {
if rf, ok := p.conn.(io.ReaderFrom); ok {
return rf.ReadFrom(r)
}
return io.Copy(p.conn, r)
}
func (p *Conn) WriteTo(w io.Writer) (int64, error) {
var err error
p.once.Do(func() { err = p.checkPrefix() })
if err != nil {
return 0, err
}
return p.bufReader.WriteTo(w)
}
func (p *Conn) Write(b []byte) (int, error) {
return p.conn.Write(b)
}
func (p *Conn) Close() error {
return p.conn.Close()
}
func (p *Conn) LocalAddr() net.Addr {
p.checkPrefixOnce()
if p.dstAddr != nil && !p.useConnAddr {
return p.dstAddr
}
return p.conn.LocalAddr()
}
// RemoteAddr returns the address of the client if the proxy
// protocol is being used, otherwise just returns the address of
// the socket peer. If there is an error parsing the header, the
// address of the client is not returned, and the socket is closed.
// Once implication of this is that the call could block if the
// client is slow. Using a Deadline is recommended if this is called
// before Read()
func (p *Conn) RemoteAddr() net.Addr {
p.checkPrefixOnce()
if p.srcAddr != nil && !p.useConnAddr {
return p.srcAddr
}
return p.conn.RemoteAddr()
}
func (p *Conn) SetDeadline(t time.Time) error {
return p.conn.SetDeadline(t)
}
func (p *Conn) SetReadDeadline(t time.Time) error {
return p.conn.SetReadDeadline(t)
}
func (p *Conn) SetWriteDeadline(t time.Time) error {
return p.conn.SetWriteDeadline(t)
}
func (p *Conn) checkPrefixOnce() {
p.once.Do(func() {
if err := p.checkPrefix(); err != nil && err != io.EOF {
log.Printf("[ERR] Failed to read proxy prefix: %v", err)
p.Close()
p.bufReader = bufio.NewReader(p.conn)
}
})
}
func (p *Conn) checkPrefix() error {
if p.proxyHeaderTimeout != 0 {
readDeadLine := time.Now().Add(p.proxyHeaderTimeout)
_ = p.conn.SetReadDeadline(readDeadLine)
defer func() {
_ = p.conn.SetReadDeadline(time.Time{})
}()
}
// Incrementally check each byte of the prefix
for i := 1; i <= prefixLen; i++ {
inp, err := p.bufReader.Peek(i)
if err != nil {
if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
return nil
} else {
return err
}
}
// Check for a prefix mis-match, quit early
if !bytes.Equal(inp, prefix[:i]) {
return nil
}
}
// Read the header line
header, err := p.bufReader.ReadString('\n')
if err != nil {
p.conn.Close()
return err
}
// Strip the carriage return and new line
header = header[:len(header)-2]
// Split on spaces, should be (PROXY <type> <src addr> <dst addr> <src port> <dst port>)
parts := strings.Split(header, " ")
if len(parts) < 2 {
p.conn.Close()
return fmt.Errorf("Invalid header line: %s", header)
}
// Verify the type is known
switch parts[1] {
case "UNKNOWN":
if !p.unknownOK || len(parts) != 2 {
p.conn.Close()
return fmt.Errorf("Invalid UNKNOWN header line: %s", header)
}
p.useConnAddr = true
return nil
case "TCP4":
case "TCP6":
default:
p.conn.Close()
return fmt.Errorf("Unhandled address type: %s", parts[1])
}
if len(parts) != 6 {
p.conn.Close()
return fmt.Errorf("Invalid header line: %s", header)
}
// Parse out the source address
ip := net.ParseIP(parts[2])
if ip == nil {
p.conn.Close()
return fmt.Errorf("Invalid source ip: %s", parts[2])
}
port, err := strconv.Atoi(parts[4])
if err != nil {
p.conn.Close()
return fmt.Errorf("Invalid source port: %s", parts[4])
}
p.srcAddr = &net.TCPAddr{IP: ip, Port: port}
// Parse out the destination address
ip = net.ParseIP(parts[3])
if ip == nil {
p.conn.Close()
return fmt.Errorf("Invalid destination ip: %s", parts[3])
}
port, err = strconv.Atoi(parts[5])
if err != nil {
p.conn.Close()
return fmt.Errorf("Invalid destination port: %s", parts[5])
}
p.dstAddr = &net.TCPAddr{IP: ip, Port: port}
return nil
}

View File

@@ -0,0 +1,486 @@
// copy from: https://github.com/armon/go-proxyproto/blob/master/protocol_test.go
package proxyproto
import (
"bytes"
"io"
"net"
"testing"
"time"
)
const (
goodAddr = "127.0.0.1"
badAddr = "127.0.0.2"
errAddr = "9999.0.0.2"
)
var (
checkAddr string
)
func TestPassthrough(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %v", err)
}
pl := &Listener{Listener: l}
go func() {
conn, err := net.Dial("tcp", pl.Addr().String())
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
conn.Write([]byte("ping"))
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recv, []byte("pong")) {
t.Fatalf("bad: %v", recv)
}
}()
conn, err := pl.Accept()
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recv, []byte("ping")) {
t.Fatalf("bad: %v", recv)
}
if _, err := conn.Write([]byte("pong")); err != nil {
t.Fatalf("err: %v", err)
}
}
func TestTimeout(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %v", err)
}
clientWriteDelay := 200 * time.Millisecond
proxyHeaderTimeout := 50 * time.Millisecond
pl := &Listener{Listener: l, ProxyHeaderTimeout: proxyHeaderTimeout}
go func() {
conn, err := net.Dial("tcp", pl.Addr().String())
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
// Do not send data for a while
time.Sleep(clientWriteDelay)
conn.Write([]byte("ping"))
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recv, []byte("pong")) {
t.Fatalf("bad: %v", recv)
}
}()
conn, err := pl.Accept()
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
// Check the remote addr is the original 127.0.0.1
remoteAddrStartTime := time.Now()
addr := conn.RemoteAddr().(*net.TCPAddr)
if addr.IP.String() != "127.0.0.1" {
t.Fatalf("bad: %v", addr)
}
remoteAddrDuration := time.Since(remoteAddrStartTime)
// Check RemoteAddr() call did timeout
if remoteAddrDuration >= clientWriteDelay {
t.Fatalf("RemoteAddr() took longer than the specified timeout: %v < %v", proxyHeaderTimeout, remoteAddrDuration)
}
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recv, []byte("ping")) {
t.Fatalf("bad: %v", recv)
}
if _, err := conn.Write([]byte("pong")); err != nil {
t.Fatalf("err: %v", err)
}
}
func TestParse_ipv4(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %v", err)
}
pl := &Listener{Listener: l}
go func() {
conn, err := net.Dial("tcp", pl.Addr().String())
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
// Write out the header!
header := "PROXY TCP4 10.1.1.1 20.2.2.2 1000 2000\r\n"
conn.Write([]byte(header))
conn.Write([]byte("ping"))
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recv, []byte("pong")) {
t.Fatalf("bad: %v", recv)
}
}()
conn, err := pl.Accept()
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recv, []byte("ping")) {
t.Fatalf("bad: %v", recv)
}
if _, err := conn.Write([]byte("pong")); err != nil {
t.Fatalf("err: %v", err)
}
// Check the remote addr
addr := conn.RemoteAddr().(*net.TCPAddr)
if addr.IP.String() != "10.1.1.1" {
t.Fatalf("bad: %v", addr)
}
if addr.Port != 1000 {
t.Fatalf("bad: %v", addr)
}
}
func TestParse_ipv6(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %v", err)
}
pl := &Listener{Listener: l}
go func() {
conn, err := net.Dial("tcp", pl.Addr().String())
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
// Write out the header!
header := "PROXY TCP6 ffff::ffff ffff::ffff 1000 2000\r\n"
conn.Write([]byte(header))
conn.Write([]byte("ping"))
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recv, []byte("pong")) {
t.Fatalf("bad: %v", recv)
}
}()
conn, err := pl.Accept()
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recv, []byte("ping")) {
t.Fatalf("bad: %v", recv)
}
if _, err := conn.Write([]byte("pong")); err != nil {
t.Fatalf("err: %v", err)
}
// Check the remote addr
addr := conn.RemoteAddr().(*net.TCPAddr)
if addr.IP.String() != "ffff::ffff" {
t.Fatalf("bad: %v", addr)
}
if addr.Port != 1000 {
t.Fatalf("bad: %v", addr)
}
}
func TestParse_Unknown(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %v", err)
}
pl := &Listener{Listener: l, UnknownOK: true}
go func() {
conn, err := net.Dial("tcp", pl.Addr().String())
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
// Write out the header!
header := "PROXY UNKNOWN\r\n"
conn.Write([]byte(header))
conn.Write([]byte("ping"))
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recv, []byte("pong")) {
t.Fatalf("bad: %v", recv)
}
}()
conn, err := pl.Accept()
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recv, []byte("ping")) {
t.Fatalf("bad: %v", recv)
}
if _, err := conn.Write([]byte("pong")); err != nil {
t.Fatalf("err: %v", err)
}
}
func TestParse_BadHeader(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %v", err)
}
pl := &Listener{Listener: l}
go func() {
conn, err := net.Dial("tcp", pl.Addr().String())
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
// Write out the header!
header := "PROXY TCP4 what 127.0.0.1 1000 2000\r\n"
conn.Write([]byte(header))
conn.Write([]byte("ping"))
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err == nil {
t.Fatalf("err: %v", err)
}
}()
conn, err := pl.Accept()
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
// Check the remote addr, should be the local addr
addr := conn.RemoteAddr().(*net.TCPAddr)
if addr.IP.String() != "127.0.0.1" {
t.Fatalf("bad: %v", addr)
}
// Read should fail
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err == nil {
t.Fatalf("err: %v", err)
}
}
func TestParse_ipv4_checkfunc(t *testing.T) {
checkAddr = goodAddr
testParse_ipv4_checkfunc(t)
checkAddr = badAddr
testParse_ipv4_checkfunc(t)
checkAddr = errAddr
testParse_ipv4_checkfunc(t)
}
func testParse_ipv4_checkfunc(t *testing.T) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %v", err)
}
checkFunc := func(addr net.Addr) (bool, error) {
tcpAddr := addr.(*net.TCPAddr)
if tcpAddr.IP.String() == checkAddr {
return true, nil
}
return false, nil
}
pl := &Listener{Listener: l, SourceCheck: checkFunc}
go func() {
conn, err := net.Dial("tcp", pl.Addr().String())
if err != nil {
t.Fatalf("err: %v", err)
}
defer conn.Close()
// Write out the header!
header := "PROXY TCP4 10.1.1.1 20.2.2.2 1000 2000\r\n"
conn.Write([]byte(header))
conn.Write([]byte("ping"))
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recv, []byte("pong")) {
t.Fatalf("bad: %v", recv)
}
}()
conn, err := pl.Accept()
if err != nil {
if checkAddr == badAddr {
return
}
t.Fatalf("err: %v", err)
}
defer conn.Close()
recv := make([]byte, 4)
_, err = conn.Read(recv)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(recv, []byte("ping")) {
t.Fatalf("bad: %v", recv)
}
if _, err := conn.Write([]byte("pong")); err != nil {
t.Fatalf("err: %v", err)
}
// Check the remote addr
addr := conn.RemoteAddr().(*net.TCPAddr)
switch checkAddr {
case goodAddr:
if addr.IP.String() != "10.1.1.1" {
t.Fatalf("bad: %v", addr)
}
if addr.Port != 1000 {
t.Fatalf("bad: %v", addr)
}
case badAddr:
if addr.IP.String() != "127.0.0.1" {
t.Fatalf("bad: %v", addr)
}
if addr.Port == 1000 {
t.Fatalf("bad: %v", addr)
}
}
}
type testConn struct {
readFromCalledWith io.Reader
net.Conn // nil; crash on any unexpected use
}
func (c *testConn) ReadFrom(r io.Reader) (int64, error) {
c.readFromCalledWith = r
return 0, nil
}
func (c *testConn) Write(p []byte) (int, error) {
return len(p), nil
}
func (c *testConn) Read(p []byte) (int, error) {
return 1, nil
}
func TestCopyToWrappedConnection(t *testing.T) {
innerConn := &testConn{}
wrappedConn := NewConn(innerConn, 0)
dummySrc := &testConn{}
io.Copy(wrappedConn, dummySrc)
if innerConn.readFromCalledWith != dummySrc {
t.Error("Expected io.Copy to delegate to ReadFrom function of inner destination connection")
}
}
func TestCopyFromWrappedConnection(t *testing.T) {
wrappedConn := NewConn(&testConn{}, 0)
dummyDst := &testConn{}
io.Copy(dummyDst, wrappedConn)
if dummyDst.readFromCalledWith != wrappedConn.conn {
t.Errorf("Expected io.Copy to pass inner source connection to ReadFrom method of destination")
}
}
func TestCopyFromWrappedConnectionToWrappedConnection(t *testing.T) {
innerConn1 := &testConn{}
wrappedConn1 := NewConn(innerConn1, 0)
innerConn2 := &testConn{}
wrappedConn2 := NewConn(innerConn2, 0)
io.Copy(wrappedConn1, wrappedConn2)
if innerConn1.readFromCalledWith != innerConn2 {
t.Errorf("Expected io.Copy to pass inner source connection to ReadFrom of inner destination connection")
}
}

View File

@@ -0,0 +1,40 @@
package utils
import (
"crypto/rand"
"encoding/base64"
mt "math/rand"
"golang.org/x/crypto/bcrypt"
)
func PasswordHash(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func PasswordVerify(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// $sha-256$salt-key$hash-abcd
// $sha-512$salt-key$hash-abcd
const (
saltSize = 16
delmiter = "$"
)
func RandSecret(min int, max int) (string, error) {
rb := make([]byte, randInt(min, max))
_, err := rand.Read(rb)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(rb), nil
}
func randInt(min int, max int) int {
return min + mt.Intn(max-min)
}

74
server/pkg/utils/util.go Normal file
View File

@@ -0,0 +1,74 @@
package utils
import (
"fmt"
"math/rand"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func InArrStr(arr []string, str string) bool {
for _, d := range arr {
if d == str {
return true
}
}
return false
}
const (
KB = 1024
MB = 1024 * KB
GB = 1024 * MB
TB = 1024 * GB
PB = 1024 * TB
)
func HumanByte(bf interface{}) string {
var hb string
var bAll float64
switch bi := bf.(type) {
case int:
bAll = float64(bi)
case int32:
bAll = float64(bi)
case uint32:
bAll = float64(bi)
case int64:
bAll = float64(bi)
case uint64:
bAll = float64(bi)
case float64:
bAll = float64(bi)
}
switch {
case bAll >= TB:
hb = fmt.Sprintf("%0.2f TB", bAll/TB)
case bAll >= GB:
hb = fmt.Sprintf("%0.2f GB", bAll/GB)
case bAll >= MB:
hb = fmt.Sprintf("%0.2f MB", bAll/MB)
case bAll >= KB:
hb = fmt.Sprintf("%0.2f KB", bAll/KB)
default:
hb = fmt.Sprintf("%0.2f B", bAll)
}
return hb
}
func RandomNum(length int) string {
letterRunes := []rune("abcdefghijklmnpqrstuvwxy1234567890")
bytes := make([]rune, length)
for i := range bytes {
bytes[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(bytes)
}

View File

@@ -0,0 +1,29 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestInArrStr(t *testing.T) {
assert := assert.New(t)
arr := []string{"a", "b", "c"}
assert.True(InArrStr(arr, "b"))
assert.False(InArrStr(arr, "d"))
}
func TestHumanByte(t *testing.T) {
assert := assert.New(t)
var s string
s = HumanByte(999)
assert.Equal(s, "999.00 B")
s = HumanByte(10256)
assert.Equal(s, "10.02 KB")
s = HumanByte(99 * 1024 * 1024)
assert.Equal(s, "99.00 MB")
s = HumanByte(1023 * 1024 * 1024)
assert.Equal(s, "1023.00 MB")
s = HumanByte(1024 * 1024 * 1024)
assert.Equal(s, "1.00 GB")
}

View File

@@ -0,0 +1,51 @@
package sessdata
import (
"fmt"
"reflect"
)
// 用b的所有字段覆盖a的
// 如果fields不为空, 表示用b的特定字段覆盖a的
// a应该为结构体指针
func CopyStruct(a interface{}, b interface{}, fields ...string) (err error) {
at := reflect.TypeOf(a)
av := reflect.ValueOf(a)
bt := reflect.TypeOf(b)
bv := reflect.ValueOf(b)
// 简单判断下
if at.Kind() != reflect.Ptr {
err = fmt.Errorf("a must be a struct pointer")
return
}
av = reflect.ValueOf(av.Interface())
// 要复制哪些字段
_fields := make([]string, 0)
if len(fields) > 0 {
_fields = fields
} else {
for i := 0; i < bv.NumField(); i++ {
_fields = append(_fields, bt.Field(i).Name)
}
}
if len(_fields) == 0 {
fmt.Println("no fields to copy")
return
}
// 复制
for i := 0; i < len(_fields); i++ {
name := _fields[i]
f := av.Elem().FieldByName(name)
bValue := bv.FieldByName(name)
// a中有同名的字段并且类型一致才复制
if f.IsValid() && f.Kind() == bValue.Kind() {
f.Set(bValue)
}
}
return
}

View File

@@ -0,0 +1,38 @@
package sessdata
import (
"testing"
"github.com/stretchr/testify/assert"
)
type A struct {
Id int
Name string
Age int
Addr string
}
type B struct {
IdB int
NameB string
Age int
Addr string
}
func TestCopyStruct(t *testing.T) {
assert := assert.New(t)
a := A{
Id: 1,
Name: "bob",
Age: 15,
Addr: "American",
}
b := B{}
err := CopyStruct(&b, a)
assert.Nil(err)
assert.Equal(b.IdB, 0)
assert.Equal(b.NameB, "")
assert.Equal(b.Age, 15)
assert.Equal(b.Addr, "American")
}

167
server/sessdata/ip_pool.go Normal file
View File

@@ -0,0 +1,167 @@
package sessdata
import (
"encoding/binary"
"net"
"sync"
"time"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata"
)
var (
IpPool = &ipPoolConfig{}
ipActive = map[string]bool{}
)
type ipPoolConfig struct {
mux sync.Mutex
// 计算动态ip
Ipv4Gateway net.IP
Ipv4Mask net.IP
Ipv4IPNet *net.IPNet
IpLongMin uint32
IpLongMax uint32
}
func initIpPool() {
// 地址处理
_, ipNet, err := net.ParseCIDR(base.Cfg.Ipv4CIDR)
if err != nil {
panic(err)
}
IpPool.Ipv4IPNet = ipNet
IpPool.Ipv4Mask = net.IP(ipNet.Mask)
IpPool.Ipv4Gateway = net.ParseIP(base.Cfg.Ipv4Gateway)
// 网络地址零值
// zero := binary.BigEndian.Uint32(ip.Mask(mask))
// 广播地址
// one, _ := ipNet.Mask.Size()
// max := min | uint32(math.Pow(2, float64(32-one))-1)
// ip地址池
IpPool.IpLongMin = ip2long(net.ParseIP(base.Cfg.Ipv4Pool[0]))
IpPool.IpLongMax = ip2long(net.ParseIP(base.Cfg.Ipv4Pool[1]))
}
func long2ip(i uint32) net.IP {
ip := make([]byte, 4)
binary.BigEndian.PutUint32(ip, i)
return ip
}
func ip2long(ip net.IP) uint32 {
ip = ip.To4()
return binary.BigEndian.Uint32(ip)
}
// 获取动态ip
func AcquireIp(username, macAddr string) net.IP {
IpPool.mux.Lock()
defer IpPool.mux.Unlock()
tNow := time.Now()
// 判断已经分配过
mi := &dbdata.IpMap{}
err := dbdata.One("MacAddr", macAddr, mi)
if err == nil {
ip := mi.IpAddr
ipStr := ip.String()
// 检测原有ip是否在新的ip池内
if IpPool.Ipv4IPNet.Contains(ip) {
mi.Username = username
mi.LastLogin = tNow
// 回写db数据
_ = dbdata.Save(mi)
ipActive[ipStr] = true
return ip
} else {
_ = dbdata.Del(mi)
}
}
// 全局遍历未分配ip
// 优先获取没有使用的ip
for i := IpPool.IpLongMin; i <= IpPool.IpLongMax; i++ {
ip := long2ip(i)
ipStr := ip.String()
mi := &dbdata.IpMap{}
err := dbdata.One("IpAddr", ip, mi)
if err != nil && dbdata.CheckErrNotFound(err) {
// 该ip没有被使用
mi := &dbdata.IpMap{IpAddr: ip, MacAddr: macAddr, Username: username, LastLogin: tNow}
_ = dbdata.Save(mi)
ipActive[ipStr] = true
return ip
}
}
farIp := &dbdata.IpMap{LastLogin: tNow}
// 遍历超过租期ip
for i := IpPool.IpLongMin; i <= IpPool.IpLongMax; i++ {
ip := long2ip(i)
ipStr := ip.String()
// 跳过活跃连接
if _, ok := ipActive[ipStr]; ok {
continue
}
v := &dbdata.IpMap{}
err := dbdata.One("IpAddr", ip, v)
if err != nil {
base.Error(err)
return nil
}
if v.Keep {
continue
}
// 已经超过租期
if tNow.Sub(v.LastLogin) > time.Duration(base.Cfg.IpLease)*time.Second {
_ = dbdata.Del(v)
mi := &dbdata.IpMap{IpAddr: ip, MacAddr: macAddr, Username: username, LastLogin: tNow}
// 重写db数据
_ = dbdata.Save(mi)
ipActive[ipStr] = true
return ip
}
// 其他情况判断最早登陆
if v.LastLogin.Before(farIp.LastLogin) {
farIp = v
}
}
// 全都在线,没有数据可用
if farIp.Id == 0 {
return nil
}
// 使用最早登陆的mac ip
ip := farIp.IpAddr
ipStr := ip.String()
mi = &dbdata.IpMap{IpAddr: ip, MacAddr: macAddr, Username: username, LastLogin: tNow}
// 回写db数据
_ = dbdata.Save(mi)
ipActive[ipStr] = true
return ip
}
// 回收ip
func ReleaseIp(ip net.IP, macAddr string) {
IpPool.mux.Lock()
defer IpPool.mux.Unlock()
delete(ipActive, ip.String())
mi := &dbdata.IpMap{}
err := dbdata.One("IpAddr", ip, mi)
if err == nil {
mi.LastLogin = time.Now()
_ = dbdata.Save(mi)
}
}

View File

@@ -0,0 +1,64 @@
package sessdata
import (
"fmt"
"net"
"os"
"path"
"testing"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata"
"github.com/stretchr/testify/assert"
)
func preData(tmpDir string) {
base.Test()
tmpDb := path.Join(tmpDir, "test.db")
base.Cfg.DbFile = tmpDb
base.Cfg.Ipv4CIDR = "192.168.3.0/24"
base.Cfg.Ipv4Pool = []string{"192.168.3.1", "192.168.3.199"}
base.Cfg.MaxClient = 100
base.Cfg.MaxUserClient = 3
dbdata.Start()
group := dbdata.Group{
Name: "group1",
Bandwidth: 1000,
}
_ = dbdata.Save(&group)
initIpPool()
}
func cleardata(tmpDir string) {
_ = dbdata.Stop()
tmpDb := path.Join(tmpDir, "test.db")
os.Remove(tmpDb)
}
func TestIpPool(t *testing.T) {
assert := assert.New(t)
tmp := t.TempDir()
preData(tmp)
defer cleardata(tmp)
var ip net.IP
for i := 1; i <= 100; i++ {
_ = AcquireIp("user", fmt.Sprintf("mac-%d", i))
}
ip = AcquireIp("user", "mac-new")
assert.True(net.IPv4(192, 168, 3, 101).Equal(ip))
for i := 102; i <= 199; i++ {
ip = AcquireIp("user", fmt.Sprintf("mac-%d", i))
}
assert.True(net.IPv4(192, 168, 3, 199).Equal(ip))
ip = AcquireIp("user", "mac-nil")
assert.Nil(ip)
ReleaseIp(net.IPv4(192, 168, 3, 88), "mac-88")
ReleaseIp(net.IPv4(192, 168, 3, 77), "mac-77")
// 从头循环获取可用ip
ip = AcquireIp("user", "mac-release-new")
assert.True(net.IPv4(192, 168, 3, 77).Equal(ip))
}

View File

@@ -0,0 +1,45 @@
package sessdata
import (
"sync"
"github.com/bjdgyc/anylink/base"
)
const limitAllKey = "__ALL__"
var (
limitClient = map[string]int{limitAllKey: 0}
limitMux = sync.Mutex{}
)
func LimitClient(user string, close bool) bool {
limitMux.Lock()
defer limitMux.Unlock()
_all := limitClient[limitAllKey]
c, ok := limitClient[user]
if !ok { // 不存在用户
limitClient[user] = 0
}
if close {
limitClient[user] = c - 1
limitClient[limitAllKey] = _all - 1
return true
}
// 全局判断
if _all >= base.Cfg.MaxClient {
return false
}
// 超出同一个用户限制
if c >= base.Cfg.MaxUserClient {
return false
}
limitClient[user] = c + 1
limitClient[limitAllKey] = _all + 1
return true
}

View File

@@ -0,0 +1,23 @@
package sessdata
import (
"context"
"golang.org/x/time/rate"
)
type LimitRater struct {
limit *rate.Limiter
}
// lim: 令牌产生速率
// burst: 允许的最大爆发速率
func NewLimitRater(lim, burst int) *LimitRater {
limit := rate.NewLimiter(rate.Limit(lim), burst)
return &LimitRater{limit: limit}
}
// bt 不能超过burst大小
func (l *LimitRater) Wait(bt int) error {
return l.limit.WaitN(context.Background(), bt)
}

View File

@@ -0,0 +1,56 @@
package sessdata
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/bjdgyc/anylink/base"
)
// func TestCheckUser(t *testing.T) {
// user["user1"] = User{Password: "7c4a8d09ca3762af61e59520943dc26494f8941b"}
// user["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) {
assert := assert.New(t)
base.Cfg.MaxClient = 2
base.Cfg.MaxUserClient = 1
res1 := LimitClient("user1", false)
res2 := LimitClient("user1", false)
res3 := LimitClient("user2", false)
res4 := LimitClient("user3", false)
res5 := LimitClient("user1", true)
assert.True(res1)
assert.False(res2)
assert.True(res3)
assert.False(res4)
assert.True(res5)
}
func TestLimitWait(t *testing.T) {
assert := assert.New(t)
limit := NewLimitRater(1, 2)
err := limit.Wait(2)
assert.Nil(err)
start := time.Now()
err = limit.Wait(2)
assert.Nil(err)
err = limit.Wait(1)
assert.Nil(err)
end := time.Now()
sub := end.Sub(start)
assert.Equal(3, int(sub.Seconds()))
}

73
server/sessdata/online.go Normal file
View File

@@ -0,0 +1,73 @@
package sessdata
import (
"bytes"
"net"
"sort"
"sync/atomic"
"time"
"github.com/bjdgyc/anylink/pkg/utils"
)
type Online struct {
Token string `json:"token"`
Username string `json:"username"`
Group string `json:"group"`
MacAddr string `json:"mac_addr"`
Ip net.IP `json:"ip"`
RemoteAddr string `json:"remote_addr"`
TunName string `json:"tun_name"`
Mtu int `json:"mtu"`
Client string `json:"client"`
BandwidthUp string `json:"bandwidth_up"`
BandwidthDown string `json:"bandwidth_down"`
BandwidthUpAll string `json:"bandwidth_up_all"`
BandwidthDownAll string `json:"bandwidth_down_all"`
LastLogin time.Time `json:"last_login"`
}
type Onlines []Online
func (o Onlines) Len() int {
return len(o)
}
func (o Onlines) Less(i, j int) bool {
return bytes.Compare(o[i].Ip, o[j].Ip) < 0
}
func (o Onlines) Swap(i, j int) {
o[i], o[j] = o[j], o[i]
}
func OnlineSess() []Online {
var datas Onlines
sessMux.Lock()
for _, v := range sessions {
v.mux.Lock()
if v.IsActive {
val := Online{
Token: v.Token,
Ip: v.CSess.IpAddr,
Username: v.Username,
Group: v.Group,
MacAddr: v.MacAddr,
RemoteAddr: v.CSess.RemoteAddr,
TunName: v.CSess.TunName,
Mtu: v.CSess.Mtu,
Client: v.CSess.Client,
BandwidthUp: utils.HumanByte(atomic.LoadUint32(&v.CSess.BandwidthUpPeriod)) + "/s",
BandwidthDown: utils.HumanByte(atomic.LoadUint32(&v.CSess.BandwidthDownPeriod)) + "/s",
BandwidthUpAll: utils.HumanByte(atomic.LoadUint32(&v.CSess.BandwidthUpAll)),
BandwidthDownAll: utils.HumanByte(atomic.LoadUint32(&v.CSess.BandwidthDownAll)),
LastLogin: v.LastLogin,
}
datas = append(datas, val)
}
v.mux.Unlock()
}
sessMux.Unlock()
sort.Sort(&datas)
return datas
}

View File

@@ -0,0 +1,71 @@
package sessdata
type LType int8
const (
LTypeEthernet LType = 1
LTypeIPData LType = 2
)
type Payload struct {
PType byte // payload types
LType LType // LinkType
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. |
+---------------------+---------------------------------------------+
*/

318
server/sessdata/session.go Normal file
View File

@@ -0,0 +1,318 @@
package sessdata
import (
"crypto/md5"
"fmt"
"math/rand"
"net"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/bjdgyc/anylink/base"
"github.com/bjdgyc/anylink/dbdata"
)
var (
// session_token -> SessUser
sessions = make(map[string]*Session)
sessMux sync.Mutex
)
// 连接sess
type ConnSession struct {
Sess *Session
MasterSecret string // dtls协议的 master_secret
IpAddr net.IP // 分配的ip地址
LocalIp net.IP
MacHw net.HardwareAddr // 客户端mac地址,从Session取出
RemoteAddr string
Mtu int
TunName string
Client string // 客户端 mobile pc
CstpDpd int
Group *dbdata.Group
Limit *LimitRater
BandwidthUp uint32 // 使用上行带宽 Byte
BandwidthDown uint32 // 使用下行带宽 Byte
BandwidthUpPeriod uint32 // 前一周期的总量
BandwidthDownPeriod uint32
BandwidthUpAll uint32 // 使用上行带宽总量
BandwidthDownAll uint32 // 使用下行带宽总量
closeOnce sync.Once
CloseChan chan struct{}
PayloadIn chan *Payload
PayloadOut chan *Payload
}
type Session struct {
mux sync.Mutex
Sid string // auth返回的 session-id
Token string // session信息的唯一token
DtlsSid string // dtls协议的 session_id
MacAddr string // 客户端mac地址
UniqueIdGlobal string // 客户端唯一标示
Username string // 用户名
Group string
AuthStep string
AuthPass string
LastLogin time.Time
IsActive bool
// 开启link需要设置的参数
CSess *ConnSession
}
func init() {
rand.Seed(time.Now().UnixNano())
}
func checkSession() {
// 检测过期的session
go func() {
if base.Cfg.SessionTimeout == 0 {
return
}
timeout := time.Duration(base.Cfg.SessionTimeout) * time.Second
tick := time.NewTicker(time.Second * 60)
for range tick.C {
sessMux.Lock()
t := time.Now()
for k, v := range sessions {
v.mux.Lock()
if !v.IsActive {
if t.Sub(v.LastLogin) > timeout {
delete(sessions, k)
}
}
v.mux.Unlock()
}
sessMux.Unlock()
}
}()
}
func GenToken() string {
// 生成32位的 token
btoken := make([]byte, 32)
rand.Read(btoken)
return fmt.Sprintf("%x", btoken)
}
func NewSession(token string) *Session {
if token == "" {
btoken := make([]byte, 32)
rand.Read(btoken)
token = fmt.Sprintf("%x", btoken)
}
// 生成 dtlsn session_id
dtlsid := make([]byte, 32)
rand.Read(dtlsid)
sess := &Session{
Sid: fmt.Sprintf("%d", time.Now().Unix()),
Token: token,
DtlsSid: fmt.Sprintf("%x", dtlsid),
LastLogin: time.Now(),
}
sessMux.Lock()
sessions[token] = sess
sessMux.Unlock()
return sess
}
func (s *Session) NewConn() *ConnSession {
s.mux.Lock()
active := s.IsActive
macAddr := s.MacAddr
username := s.Username
s.mux.Unlock()
if active {
s.CSess.Close()
}
limit := LimitClient(username, false)
if !limit {
return nil
}
// 获取客户端mac地址
macHw, err := net.ParseMAC(macAddr)
if err != nil {
sum := md5.Sum([]byte(s.UniqueIdGlobal))
macHw = sum[0:5] // 5个byte
macHw = append([]byte{0x02}, macHw...)
macAddr = macHw.String()
}
ip := AcquireIp(username, macAddr)
if ip == nil {
LimitClient(username, true)
return nil
}
cSess := &ConnSession{
Sess: s,
MacHw: macHw,
IpAddr: ip,
closeOnce: sync.Once{},
CloseChan: make(chan struct{}),
PayloadIn: make(chan *Payload),
PayloadOut: make(chan *Payload),
}
// 查询group信息
group := &dbdata.Group{}
err = dbdata.One("Name", s.Group, group)
if err != nil {
base.Error(err)
cSess.Close()
return nil
}
cSess.Group = group
if group.Bandwidth > 0 {
// 限流设置
cSess.Limit = NewLimitRater(group.Bandwidth, group.Bandwidth)
}
go cSess.ratePeriod()
s.mux.Lock()
s.MacAddr = macAddr
s.IsActive = true
s.CSess = cSess
s.mux.Unlock()
return cSess
}
func (cs *ConnSession) Close() {
cs.closeOnce.Do(func() {
base.Info("closeOnce:", cs.IpAddr)
cs.Sess.mux.Lock()
defer cs.Sess.mux.Unlock()
close(cs.CloseChan)
cs.Sess.IsActive = false
cs.Sess.LastLogin = time.Now()
cs.Sess.CSess = nil
ReleaseIp(cs.IpAddr, cs.Sess.MacAddr)
LimitClient(cs.Sess.Username, true)
})
}
const BandwidthPeriodSec = 2 // 流量速率统计周期(秒)
func (cs *ConnSession) ratePeriod() {
tick := time.NewTicker(time.Second * BandwidthPeriodSec)
defer tick.Stop()
for range tick.C {
select {
case <-cs.CloseChan:
return
default:
}
// 实时流量清零
rtUp := atomic.SwapUint32(&cs.BandwidthUp, 0)
rtDown := atomic.SwapUint32(&cs.BandwidthDown, 0)
// 设置上一周期每秒的流量
atomic.SwapUint32(&cs.BandwidthUpPeriod, rtUp/BandwidthPeriodSec)
atomic.SwapUint32(&cs.BandwidthDownPeriod, rtDown/BandwidthPeriodSec)
// 累加所有流量
atomic.AddUint32(&cs.BandwidthUpAll, rtUp)
atomic.AddUint32(&cs.BandwidthDownAll, rtDown)
}
}
const MaxMtu = 1460
func (cs *ConnSession) SetMtu(mtu string) {
cs.Mtu = MaxMtu
mi, err := strconv.Atoi(mtu)
if err != nil || mi < 100 {
return
}
if mi < MaxMtu {
cs.Mtu = mi
}
}
func (cs *ConnSession) SetTunName(name string) {
cs.Sess.mux.Lock()
defer cs.Sess.mux.Unlock()
cs.TunName = name
}
func (cs *ConnSession) RateLimit(byt int, isUp bool) error {
if isUp {
atomic.AddUint32(&cs.BandwidthUp, uint32(byt))
return nil
}
// 只对下行速率限制
atomic.AddUint32(&cs.BandwidthDown, uint32(byt))
if cs.Limit == nil {
return nil
}
return cs.Limit.Wait(byt)
}
func SToken2Sess(stoken string) *Session {
stoken = strings.TrimSpace(stoken)
sarr := strings.Split(stoken, "@")
token := sarr[1]
return Token2Sess(token)
}
func Token2Sess(token string) *Session {
sessMux.Lock()
defer sessMux.Unlock()
return sessions[token]
}
func Dtls2Sess(dtlsid []byte) *Session {
return nil
}
func DelSess(token string) {
// sessions.Delete(token)
}
func CloseSess(token string) {
sessMux.Lock()
defer sessMux.Unlock()
sess, ok := sessions[token]
if !ok {
return
}
delete(sessions, token)
sess.CSess.Close()
}
func CloseCSess(token string) {
sessMux.Lock()
defer sessMux.Unlock()
sess, ok := sessions[token]
if !ok {
return
}
sess.CSess.Close()
}
func DelSessByStoken(stoken string) {
stoken = strings.TrimSpace(stoken)
sarr := strings.Split(stoken, "@")
token := sarr[1]
sessMux.Lock()
delete(sessions, token)
sessMux.Unlock()
}

View File

@@ -0,0 +1,38 @@
package sessdata
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewSession(t *testing.T) {
ast := assert.New(t)
sessions = make(map[string]*Session)
sess := NewSession("")
token := sess.Token
v, ok := sessions[token]
ast.True(ok)
ast.Equal(sess, v)
}
func TestConnSession(t *testing.T) {
ast := assert.New(t)
tmp := t.TempDir()
preData(tmp)
defer cleardata(tmp)
sess := NewSession("")
sess.Group = "group1"
sess.MacAddr = "00:15:5d:50:14:43"
cSess := sess.NewConn()
err := cSess.RateLimit(100, true)
ast.Nil(err)
ast.Equal(cSess.BandwidthUp, uint32(100))
err = cSess.RateLimit(200, false)
ast.Nil(err)
ast.Equal(cSess.BandwidthDown, uint32(200))
cSess.Close()
}

6
server/sessdata/start.go Normal file
View File

@@ -0,0 +1,6 @@
package sessdata
func Start() {
initIpPool()
checkSession()
}