This commit is contained in:
jasinco 2025-11-20 11:47:00 +08:00
commit 203d986e08
17 changed files with 3312 additions and 0 deletions

52
api/auth/handlr.go Normal file
View file

@ -0,0 +1,52 @@
package auth
import (
"errors"
"git.jasinco.work/wgcl/internal/federate"
"git.jasinco.work/wgcl/internal/logger"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
var Driver federate.AUTH = nil
type login struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
var ErrBadRequest = errors.New("Failed to parse request")
var ErrNoDriver = errors.New("No Auth Driver")
func UserLogin(c *gin.Context) (*User, error) {
var cred login
if c.ShouldBind(&cred) != nil {
return nil, ErrBadRequest
}
if Driver == nil {
return nil, ErrNoDriver
}
userdn, err := Driver.UserAuthenticate(cred.Username, cred.Password)
if err == nil {
var power uint16 = 0
if stat, err := Driver.UserAuthorizeWG(userdn); err == nil {
if stat {
power |= AuthorizeWG
}
} else {
logger.Logger.Error(err.Error())
}
logger.Logger.Info("user authorize", zap.String("user", userdn), zap.Uint16("power", power))
u := &User{
UserName: userdn,
Authorized: power,
}
u.setupClaim()
return u, nil
}
return nil, err
}

88
api/auth/jwt.go Normal file
View file

@ -0,0 +1,88 @@
package auth
import (
"errors"
"strings"
"time"
"git.jasinco.work/wgcl/internal/logger"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"go.uber.org/zap"
)
var AuthorizeWG uint16 = 0x1
var AuthorizeRevProxy uint16 = 0x2
var JWTKey = []byte("fecv")
type User struct {
UserName string `json:"dn" binding:"required"`
Authorized uint16 `json:"pwr" binding:"required"`
jwt.RegisteredClaims
}
func (u *User) setupClaim() {
u.ExpiresAt = jwt.NewNumericDate(time.Now().Add(24 * time.Hour))
u.Issuer = "tethsues"
u.Subject = "smb"
u.ID = "1"
u.Audience = nil
}
func AuthMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
logger.Logger.Info("auth middleware first")
var user *User = nil
auth_head, auth_head_exist := ctx.Request.Header["Authorization"]
// cookie,cookie_err := ctx.Request.Cookie("token")
if auth_head_exist && strings.HasPrefix(auth_head[0], "Bearer ") {
_token, err := jwt.ParseWithClaims(strings.TrimPrefix(auth_head[0], "Bearer "), &User{}, func(t *jwt.Token) (any, error) {
return JWTKey, nil
})
if err != nil {
ctx.Status(400)
ctx.Abort()
return
}
if _user, ok := _token.Claims.(*User); ok {
user = _user
}
}
if user != nil {
logger.Logger.Info("auth middleware", zap.String("dn", user.UserName))
ctx.Set("token", user)
} else {
ctx.Set("token", nil)
ctx.Status(400)
ctx.Abort()
return
}
ctx.Next()
}
}
func IssueJWT(c *gin.Context) {
cred, err := UserLogin(c)
if errors.Is(err, ErrBadRequest) {
c.Status(400)
return
} else if err != nil {
logger.Logger.Warn(err.Error())
c.Status(500)
return
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, *cred)
ss, err := token.SignedString(JWTKey)
if err != nil {
logger.Logger.Warn(err.Error())
c.Status(500)
return
}
c.JSON(200, gin.H{
"token": ss,
})
// c.SetCookie("credential", )
}

1
api/vpn/controller.go Normal file
View file

@ -0,0 +1 @@
package vpn

142
api/vpn/handler.go Normal file
View file

@ -0,0 +1,142 @@
package vpn
import (
"context"
"io"
"net/netip"
"time"
"git.jasinco.work/wgcl/internal/logger"
"git.jasinco.work/wgcl/internal/wg"
protovpn "git.jasinco.work/wgcl/proto/out"
"github.com/gin-gonic/gin"
"google.golang.org/protobuf/encoding/protojson"
)
var wgctrl *wg.WG
var sync_cancel context.CancelFunc
func SyncWGStat(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
wgctrl.SyncStatus()
logger.Logger.Info("sync wg status")
time.Sleep(5 * time.Second)
}
}
}
func StartWG() {
var err error
baseip4 := netip.AddrFrom4([4]byte{172, 16, 0, 1})
pool := wg.IPPool_Init(baseip4, 26)
wgctrl, err = wg.WG_init("wg0", wg.NewKey(), 51823, pool)
if err != nil {
logger.Logger.Warn(err.Error())
logger.Logger.Fatal("Failed init wgctrl")
}
ctx, cancel := context.WithCancel(context.Background())
sync_cancel = cancel
go SyncWGStat(ctx)
}
func StopWG() {
logger.Logger.Info("Stopping WG")
sync_cancel()
wgctrl.Deinit()
}
func GETPeers(c *gin.Context) {
resp := wgctrl.GetPeersJson()
resp_json, err := protojson.Marshal(resp)
if err != nil {
c.Status(500)
return
}
c.Data(200, "application/json", resp_json)
}
func NewPeer(c *gin.Context) {
req := &protovpn.PeerReq{}
req_json, err := io.ReadAll(c.Request.Body)
if err != nil {
c.Status(400)
return
}
if protojson.Unmarshal(req_json, req) != nil {
c.Status(400)
return
}
wgctrl.WriteTxStart()
defer wgctrl.WriteTxEnd()
var ip4 *netip.Addr
if req.HasIp4() {
_ip4, err := netip.ParseAddr(req.GetIp4())
ip4 = &_ip4
if err != nil {
c.Status(400)
return
}
} else {
_ip4, err := wgctrl.IP4Pool.Next()
if err != nil {
logger.Logger.Warn("ip pool exhausted")
c.Status(500)
return
}
ip4 = &_ip4
}
peer := wg.WGPeer_init(req.GetName(), wg.NewKey(), *ip4)
wgctrl.AddPeer(peer)
wgctrl.IP4Pool.Lease(*ip4)
wgctrl.Apply()
c.Status(200)
}
func SearchPeer(c *gin.Context) {
if pubkey := c.Query("pubkey"); len(pubkey) > 0 {
p, err := wgctrl.SearchPeerPubkey(pubkey)
if p == nil {
if err != nil {
logger.Logger.Warn(err.Error())
}
c.Status(400)
return
}
b, err := protojson.Marshal(p.ToPeerStatus())
if err != nil {
logger.Logger.Warn(err.Error())
}
c.Data(200, "application/json", b)
} else {
c.Status(400)
}
}
func DeletePeer(c *gin.Context) {
if pubkey := c.Query("pubkey"); len(pubkey) > 0 {
p, err := wgctrl.SearchPeerPubkey(pubkey)
if p == nil {
if err != nil {
logger.Logger.Warn(err.Error())
}
c.Status(400)
return
}
wgctrl.WriteTxStart()
defer wgctrl.WriteTxEnd()
p.Disable()
if err = wgctrl.Apply(); err != nil {
logger.Logger.Warn("Can't apply disable peer")
}
wgctrl.RemovePeer(p)
c.Status(200)
} else {
c.Status(400)
}
}

52
cmd/main.go Normal file
View file

@ -0,0 +1,52 @@
package main
import (
"time"
"git.jasinco.work/wgcl/api/auth"
"git.jasinco.work/wgcl/api/vpn"
"git.jasinco.work/wgcl/internal/federate"
"git.jasinco.work/wgcl/internal/logger"
"github.com/fvbock/endless"
//"github.com/gin-contrib/pprof"
ginzap "github.com/gin-contrib/zap"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Use(ginzap.Ginzap(logger.Logger, time.RFC3339, true))
// r.Use(ginzap.RecoveryWithZap(logger.Logger, true))
defer logger.Flush()
l := federate.FedLDAP{
Base_dn: "dc=example,dc=com",
User_filter: "(&(objectClass=person)(uid=%s))",
Group_wg_filter: "(&(dn=%s)(memberof=cn=wgopr,ou=groups,dc=example,dc=com))",
}
l.Init("ldap://localhost:3890")
if err := l.AuthBind("uid=admin,ou=people,dc=example,dc=com", "recrecrec"); err != nil {
logger.Logger.Fatal(err.Error())
}
auth.Driver = &l
vpn.StartWG()
defer vpn.StopWG()
// pprof.Register(r)
r.POST("/api/auth", auth.IssueJWT)
{
g := r.Group("/api", auth.AuthMiddleware())
v := g.Group("/vpn")
v.GET("/peers", vpn.GETPeers)
v.POST("/peer", vpn.NewPeer)
v.GET("/peer", vpn.SearchPeer)
v.DELETE("/peer", vpn.DeletePeer)
}
endless.ListenAndServe("0.0.0.0:8080", r)
}

55
go.mod Normal file
View file

@ -0,0 +1,55 @@
module git.jasinco.work/wgcl
go 1.25.4
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/appleboy/gin-jwt/v3 v3.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/pprof v1.5.3 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-contrib/zap v1.1.5 // indirect
github.com/gin-gonic/gin v1.11.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-ldap/ldap/v3 v3.4.12 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/go-set/v3 v3.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/redis/rueidis v1.0.66 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)

147
go.sum Normal file
View file

@ -0,0 +1,147 @@
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/appleboy/gin-jwt/v3 v3.2.0 h1:Ifa8Zsm2cZ93u/HIAhcTmftTK8rIaaoAoH77lcajWD0=
github.com/appleboy/gin-jwt/v3 v3.2.0/go.mod h1:ANNEPdDkdOp6jXAbicMFX7N4mIIx70m9A3asDmXdbYo=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc=
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/pprof v1.5.3 h1:Bj5SxJ3kQDVez/s/+f9+meedJIqLS+xlkIVDe/lcvgM=
github.com/gin-contrib/pprof v1.5.3/go.mod h1:0+LQSZ4SLO0B6+2n6JBzaEygpTBxe/nI+YEYpfQQ6xY=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-contrib/zap v1.1.5 h1:qKwhWb4DQgPriCl1AHLLob6hav/KUIctKXIjTmWIN3I=
github.com/gin-contrib/zap v1.1.5/go.mod h1:lAchUtGz9M2K6xDr1rwtczyDrThmSx6c9F384T45iOE=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-set/v3 v3.0.1 h1:ZwO15ZYmIrFYL9zSm2wBuwcRiHxVdp46m/XA/MUlM6I=
github.com/hashicorp/go-set/v3 v3.0.1/go.mod h1:0oPQqhtitglZeT2ZiWnRIfUG6gJAHnn7LzrS7SbgNY4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/redis/rueidis v1.0.66 h1:7rvyrl0vL/cAEkE97+L5v3MJ3Vg8IKz+KIxUTfT+yJk=
github.com/redis/rueidis v1.0.66/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

13
internal/federate/if.go Normal file
View file

@ -0,0 +1,13 @@
package federate
import "errors"
type AUTH interface {
IsUser(name string) (string, error)
UserAuthorizeWG(name string) (bool, error)
// return IsUser check
UserAuthenticate(name string, password string) (string, error)
}
var ErrAUTHNoUser = errors.New("No user for auth")
var ErrAUTHAuthenticateInvalidPassword = errors.New("Incorrect Password")

80
internal/federate/ldap.go Normal file
View file

@ -0,0 +1,80 @@
package federate
import (
"errors"
"fmt"
"git.jasinco.work/wgcl/internal/logger"
"github.com/go-ldap/ldap/v3"
)
type FedLDAP struct {
Base_dn string
User_filter string
endpoint *ldap.Conn
Group_wg_filter string
auth_endpoint *ldap.Conn
}
func (l *FedLDAP) Init(endpoint string) {
var err error
l.endpoint, err = ldap.DialURL(endpoint)
if err != nil {
logger.Logger.Fatal(err.Error())
}
l.auth_endpoint, err = ldap.DialURL(endpoint)
if err != nil {
logger.Logger.Fatal(err.Error())
}
}
func (l *FedLDAP) AuthBind(user string, pass string) error {
return l.endpoint.Bind(user, pass)
}
func (l *FedLDAP) IsUser(name string) (string, error) {
req := ldap.NewSearchRequest(l.Base_dn, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
1, 0, false,
fmt.Sprintf(l.User_filter, ldap.EscapeFilter(name)), []string{"dn"}, nil)
resp, err := l.endpoint.Search(req)
if err != nil {
return "", err
}
if len(resp.Entries) != 1 {
return "", ErrAUTHNoUser
}
return resp.Entries[0].DN, nil
}
func (l *FedLDAP) UserAuthorizeWG(user_dn string) (bool, error) {
req := ldap.NewSearchRequest(l.Base_dn, ldap.ScopeBaseObject, ldap.NeverDerefAliases,
1, 0, false,
fmt.Sprintf(l.Group_wg_filter, user_dn), []string{"dn"}, nil)
resp, err := l.endpoint.Search(req)
if err != nil {
return false, err
}
return len(resp.Entries) > 0, nil
}
func (l *FedLDAP) UserAuthenticate(name string, password string) (string, error) {
// find user
userdn, err := l.IsUser(name)
if err != nil {
return "", err
}
req := ldap.NewSimpleBindRequest(userdn, ldap.EscapeFilter(password), nil)
_, err = l.auth_endpoint.SimpleBind(req)
var ldaperr *ldap.Error
if errors.As(err, &ldaperr) && ldaperr.ResultCode == ldap.LDAPResultInvalidCredentials {
return "", ErrAUTHAuthenticateInvalidPassword
}
if err != nil {
return "", err
}
return userdn, nil
}

23
internal/logger/lib.go Normal file
View file

@ -0,0 +1,23 @@
package logger
import "go.uber.org/zap"
var Logger *zap.Logger
func init() {
Logger, _ = zap.NewProduction()
// sugar := logger.Sugar()
// sugar.Infow("failed to fetch URL",
// // Structured context as loosely typed key-value pairs.
// "url", url,
// "attempt", 3,
// "backoff", time.Second,
// )
// sugar.Infof("Failed to fetch URL: %s", url)
// Create a Gin router with default middleware (logger and recovery)
}
func Flush() {
Logger.Sync() // flushes buffer, if any
}

67
internal/wg/ip_pool.go Normal file
View file

@ -0,0 +1,67 @@
package wg
import (
"encoding/binary"
"errors"
"net/netip"
"github.com/hashicorp/go-set/v3"
)
type IPPool struct {
scope netip.Prefix
leasure *set.Set[netip.Addr]
cache netip.Addr
}
var ErrIPPoolExhausted = errors.New("IP Pool Exhausted")
// @param base_ip4: is used as the gateway address
func IPPool_Init(base_ip4 netip.Addr, cidr uint8) *IPPool {
scope := netip.PrefixFrom(base_ip4, int(cidr))
pool := &IPPool{leasure: set.New[netip.Addr](1 << (32 - cidr)), scope: scope}
pool.cache = pool.scope.Addr().Next()
//calculate broadcast address
var wildcard uint32 = (1 << (32 - cidr)) - 1
wildcard_mask := [4]byte{}
binary.BigEndian.PutUint32(wildcard_mask[:4], wildcard)
broadcast := base_ip4.As4()
broadcast[0] |= wildcard_mask[0]
broadcast[1] |= wildcard_mask[1]
broadcast[2] |= wildcard_mask[2]
broadcast[3] |= wildcard_mask[3]
broadcast_addr := netip.AddrFrom4(broadcast)
pool.Lease(broadcast_addr)
//gateway
pool.Lease(base_ip4)
return pool
}
func (p *IPPool) Next() (netip.Addr, error) {
cyclic := false
for ; ; p.cache = p.cache.Next() {
if p.leasure.Contains(p.cache) && p.cache.IsValid() {
continue
} else {
if p.cache.IsValid() && p.scope.Contains(p.cache) && !p.leasure.Contains(p.cache) {
return p.cache, nil
} else if !cyclic {
p.cache = p.scope.Masked().Addr()
cyclic = true
} else {
return p.cache, ErrIPPoolExhausted
}
}
}
}
func (p *IPPool) Lease(addr netip.Addr) {
p.leasure.Insert(addr)
}
func (p *IPPool) HasIP(addr netip.Addr) {
p.leasure.Contains(addr)
}

273
internal/wg/wg.go Normal file
View file

@ -0,0 +1,273 @@
package wg
/*
#cgo CFLAGS: -I./
#include <wireguard.h>
#include <arpa/inet.h>
#include <stdlib.h>
enum wg_device_flags dev_cfg = WGDEVICE_HAS_PRIVATE_KEY|WGDEVICE_HAS_LISTEN_PORT;
*/
import "C"
import (
"bytes"
"encoding/binary"
"errors"
"math/bits"
"net/netip"
"sync"
"unsafe"
"git.jasinco.work/wgcl/internal/logger"
protovpn "git.jasinco.work/wgcl/proto/out"
"github.com/vishvananda/netlink"
"google.golang.org/protobuf/proto"
)
// TODO: inspect all api, complete error handling
type WGPeer struct {
name string
privkey string //base64 string
peer C.wg_peer
next *WGPeer
}
type WG struct {
dev *C.wg_device
lock sync.RWMutex
keep_alive bool
peers *WGPeer
peers_len int
IP4Pool *IPPool
}
var WGfailedadddev = errors.New("failed add wgdev")
var WGfailedsetdev = errors.New("failed set wgdev")
var WGfailedparseb64key = errors.New("failed parse base64 key")
func GetB64Key(key *C.wg_key) string {
var psk_b64 C.wg_key_b64_string
psk_ptr := (*C.uint8_t)(unsafe.Pointer(&(*key)[0]))
psk_b64_ptr := (*C.char)(unsafe.Pointer(&psk_b64[0]))
C.wg_key_to_base64(psk_b64_ptr, psk_ptr)
return C.GoString(psk_b64_ptr)
}
func NewKey() string {
var privkey C.wg_key
privkey_as_ptr := (*C.uint8_t)(unsafe.Pointer(&privkey[0]))
C.wg_generate_private_key(privkey_as_ptr)
return GetB64Key(&privkey)
}
func NewPresharedKey() string {
var psk C.wg_key
psk_ptr := (*C.uint8_t)(unsafe.Pointer(&psk[0]))
C.wg_generate_preshared_key(psk_ptr)
return GetB64Key(&psk)
}
// device name and receive base64 encoded keys
func WG_init(name string, privkey string, listen uint16, ip4_pool *IPPool) (*WG, error) {
var dev WG
dev.dev = (*C.wg_device)(C.malloc(C.sizeof_wg_device))
// hence it only call once at startup, I'd rather not to free it explicitly
C.wg_key_from_base64((*C.uint8_t)(unsafe.Pointer(&dev.dev.private_key[0])), C.CString(privkey))
// set device name
ifname := C.CString(name)
C.strncpy(&dev.dev.name[0], ifname, 16)
dev.dev.flags = C.dev_cfg
dev.dev.first_peer = (*C.wg_peer)(C.NULL)
dev.dev.last_peer = (*C.wg_peer)(C.NULL)
dev.dev.listen_port = (C.uint16_t)(listen)
if C.wg_add_device((*C.char)(unsafe.Pointer(&dev.dev.name[0]))) < 0 {
return nil, WGfailedadddev
}
if C.wg_set_device(dev.dev) < 0 {
return nil, WGfailedsetdev
}
if C.wg_get_device(&dev.dev, (*C.char)(unsafe.Pointer(&dev.dev.name[0]))) < 0 {
return nil, WGfailedsetdev
}
dev.peers = nil
dev.IP4Pool = ip4_pool
// setup interface
var err error
wg_if, err := netlink.LinkByIndex(int(dev.dev.ifindex))
if err != nil {
return nil, err
}
nl_addr, err := netlink.ParseAddr(ip4_pool.scope.String())
if err != nil {
return nil, err
}
err = netlink.AddrAdd(wg_if, nl_addr)
if err != nil {
return nil, err
}
err = netlink.LinkSetUp(wg_if)
if err != nil {
return nil, err
}
return &dev, nil
}
func (wg *WG) SyncStatus() {
wg.lock.Lock()
defer wg.lock.Unlock()
if C.wg_get_device(&wg.dev, (*C.char)(unsafe.Pointer(&wg.dev.name[0]))) < 0 {
logger.Logger.Info("Failed to sync wg status")
}
}
func (wg *WG) Deinit() {
C.wg_del_device((*C.char)(unsafe.Pointer(&wg.dev.name[0])))
}
func (wg *WG) WriteTxStart() {
wg.lock.Lock()
}
func (wg *WG) WriteTxEnd() {
wg.lock.Unlock()
}
func (wg *WG) AddPeer(peer *WGPeer) {
peer.next = nil
wg.peers_len += 1
if wg.peers == nil {
wg.peers = peer
return
}
ptr := wg.peers
for ptr.next != nil {
ptr = ptr.next
}
ptr.next = peer
}
func (wg *WG) RemovePeer(peer *WGPeer) {
if wg.peers == peer {
wg.peers = peer.next
}
for ptr := wg.peers; ptr != nil; ptr = ptr.next {
if ptr.next == peer {
ptr.next = peer.next
break
}
}
wg.peers_len -= 1
}
func (wg *WG) Apply() error {
if wg.peers != nil {
first_ptr := (*C.wg_peer)(unsafe.Pointer(&wg.peers.peer))
wg.dev.first_peer = first_ptr
} else {
wg.dev.first_peer = nil
wg.dev.last_peer = nil
}
for ptr := wg.peers; ptr != nil; ptr = (*WGPeer)(ptr.next) {
if ptr.next == nil {
ptr.peer.next_peer = nil
last_ptr := (*C.wg_peer)(unsafe.Pointer(&ptr.peer))
wg.dev.last_peer = last_ptr
break
}
ptr.peer.next_peer = &(*WGPeer)(ptr.next).peer
}
wgdev := (*C.wg_device)(unsafe.Pointer(wg.dev))
if C.wg_set_device(wgdev) < 0 {
return WGfailedsetdev
}
return nil
}
func (wg *WG) SearchPeerIP4(addr netip.Addr) *WGPeer {
wg.lock.RLock()
defer wg.lock.RUnlock()
ip4_slice := addr.As4()
for ptr := wg.peers; ptr != nil; ptr = ptr.next {
if bytes.Equal(ip4_slice[:], ptr.peer.first_allowedip.anon0[:4]) {
return ptr
}
}
return nil
}
func (wg *WG) SearchPeerPubkey(pubkey string) (*WGPeer, error) {
wg.lock.RLock()
defer wg.lock.RUnlock()
pubkey_cstr := C.CString(pubkey)
defer C.free(unsafe.Pointer(pubkey_cstr))
var pubkey_bin C.wg_key
if C.wg_key_from_base64((*C.uint8_t)(&pubkey_bin[0]), pubkey_cstr) < 0 {
return nil, WGfailedparseb64key
}
for ptr := wg.peers; ptr != nil; ptr = ptr.next {
if C.wg_key_equal(&pubkey_bin, &ptr.peer.public_key) == 0 {
return ptr, nil
}
}
return nil, nil
}
func (w *WG) GetPeersJson() *protovpn.PeerResp {
w.lock.RLock()
defer w.lock.RUnlock()
peers := make([]*protovpn.PeerStatus, w.peers_len)
peers_ptr := 0
for ptr := w.peers; ptr != nil; ptr = ptr.next {
peers[peers_ptr] = ptr.ToPeerStatus()
peers_ptr += 1
}
resp := protovpn.PeerResp_builder{Peers: peers}
return resp.Build()
}
func WGPeer_init(name string, privkey string, remote netip.Addr) *WGPeer {
var peer WGPeer
peer.name = name
peer.privkey = privkey
var privkey_bin C.wg_key
priv_cstr := C.CString(privkey)
defer C.free(unsafe.Pointer(priv_cstr))
C.wg_key_from_base64((*C.uint8_t)(unsafe.Pointer(&privkey_bin[0])), priv_cstr)
C.wg_generate_public_key((*C.uint8_t)(unsafe.Pointer(&peer.peer.public_key[0])), (*C.uint8_t)(unsafe.Pointer(&privkey_bin[0])))
peer.peer.flags = C.WGPEER_HAS_PUBLIC_KEY | C.WGPEER_REPLACE_ALLOWEDIPS
remote_ip_arr := remote.As4()
remote_ip := binary.LittleEndian.Uint32(remote_ip_arr[:])
remote_endpoint := C.wg_allowed_ip_new_ip4((C.in_addr_t)(remote_ip), 32)
peer.peer.first_allowedip = remote_endpoint
peer.peer.last_allowedip = remote_endpoint
return &peer
}
func WGPeer_free(peer *WGPeer) {
C.free(unsafe.Pointer(peer.peer.first_allowedip))
}
func (p *WGPeer) Disable() {
p.peer.flags |= C.WGPEER_REMOVE_ME
}
func (p *WGPeer) Enable() {
p.peer.flags &= bits.Reverse32(C.WGPEER_REMOVE_ME)
}
func (p *WGPeer) ToPeerStatus() *protovpn.PeerStatus {
ip4, _ := netip.AddrFromSlice(p.peer.first_allowedip.anon0[:4])
return protovpn.PeerStatus_builder{
Enabled: proto.Bool((p.peer.flags & C.WGPEER_REMOVE_ME) == 0),
Pubkey: proto.String(GetB64Key(&p.peer.public_key)),
Nick: proto.String(p.name),
Ipv4: proto.String(ip4.String()),
Beacon: proto.Int64(int64(p.peer.last_handshake_time.tv_sec)),
}.Build()
}

1720
internal/wg/wireguard.c Normal file

File diff suppressed because it is too large Load diff

115
internal/wg/wireguard.h Normal file
View file

@ -0,0 +1,115 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
/*
* Copyright (C) 2015-2020 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights
* Reserved.
*/
#ifndef WIREGUARD_H
#define WIREGUARD_H
#include <net/if.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
typedef uint8_t wg_key[32];
typedef char wg_key_b64_string[((sizeof(wg_key) + 2) / 3) * 4 + 1];
/* Cross platform __kernel_timespec */
struct timespec64 {
int64_t tv_sec;
int64_t tv_nsec;
};
typedef struct wg_allowedip {
uint16_t family;
union {
struct in_addr ip4;
struct in6_addr ip6;
};
uint8_t cidr;
struct wg_allowedip *next_allowedip;
} wg_allowedip;
wg_allowedip *wg_allowed_ip_new_ip4(in_addr_t ip4, uint8_t cidr);
enum wg_peer_flags {
WGPEER_REMOVE_ME = 1U << 0,
WGPEER_REPLACE_ALLOWEDIPS = 1U << 1,
WGPEER_HAS_PUBLIC_KEY = 1U << 2,
WGPEER_HAS_PRESHARED_KEY = 1U << 3,
WGPEER_HAS_PERSISTENT_KEEPALIVE_INTERVAL = 1U << 4
};
typedef union wg_endpoint {
struct sockaddr addr;
struct sockaddr_in addr4;
struct sockaddr_in6 addr6;
} wg_endpoint;
typedef struct wg_peer {
enum wg_peer_flags flags;
wg_key public_key;
wg_key preshared_key;
wg_endpoint endpoint;
struct timespec64 last_handshake_time;
uint64_t rx_bytes, tx_bytes;
uint16_t persistent_keepalive_interval;
struct wg_allowedip *first_allowedip, *last_allowedip;
struct wg_peer *next_peer;
} wg_peer;
enum wg_device_flags {
WGDEVICE_REPLACE_PEERS = 1U << 0,
WGDEVICE_HAS_PRIVATE_KEY = 1U << 1,
WGDEVICE_HAS_PUBLIC_KEY = 1U << 2,
WGDEVICE_HAS_LISTEN_PORT = 1U << 3,
WGDEVICE_HAS_FWMARK = 1U << 4
};
typedef struct wg_device {
char name[IFNAMSIZ];
uint32_t ifindex;
enum wg_device_flags flags;
wg_key public_key;
wg_key private_key;
uint32_t fwmark;
uint16_t listen_port;
struct wg_peer *first_peer, *last_peer;
} wg_device;
#define wg_for_each_device_name(__names, __name, __len) \
for ((__name) = (__names), (__len) = 0; ((__len) = strlen(__name)); \
(__name) += (__len) + 1)
#define wg_for_each_peer(__dev, __peer) \
for ((__peer) = (__dev)->first_peer; (__peer); (__peer) = (__peer)->next_peer)
#define wg_for_each_allowedip(__peer, __allowedip) \
for ((__allowedip) = (__peer)->first_allowedip; (__allowedip); \
(__allowedip) = (__allowedip)->next_allowedip)
int wg_set_device(wg_device *dev);
int wg_get_device(wg_device **dev, const char *device_name);
int wg_add_device(const char *device_name);
int wg_del_device(const char *device_name);
void wg_free_device(wg_device *dev);
char *wg_list_device_names(void); /* first\0second\0third\0forth\0last\0\0 */
void wg_key_to_base64(wg_key_b64_string base64, const wg_key key);
int wg_key_from_base64(wg_key key, const wg_key_b64_string base64);
bool wg_key_is_zero(const wg_key key);
void wg_generate_public_key(wg_key public_key, const wg_key private_key);
void wg_generate_private_key(wg_key private_key);
void wg_generate_preshared_key(wg_key preshared_key);
int wg_key_equal(wg_key *k1, wg_key *k2);
#endif

13
justfile Normal file
View file

@ -0,0 +1,13 @@
genproto:
protoc --proto_path=proto --go_out=proto/out --go_opt=paths=source_relative vpn.proto
build:
go build -o srv ./cmd/main.go
run: build clif
sudo ./srv
clif:
sudo ip link delete wg0 || echo "no wg0"
fzuser:
#!/usr/bin/env bash
cred=$(curlie POST localhost:8080/api/auth username=gggg password=gggggggg | jq -r ".token")
echo "bearer ${cred}"
curlie POST localhost:8080/api/vpn/peer "Authorization:Bearer $cred" name="test"

452
proto/out/vpn.pb.go Normal file
View file

@ -0,0 +1,452 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc v6.33.0
// source: vpn.proto
package out
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type PeerStatus struct {
state protoimpl.MessageState `protogen:"opaque.v1"`
xxx_hidden_Pubkey *string `protobuf:"bytes,1,opt,name=pubkey"`
xxx_hidden_Ipv4 *string `protobuf:"bytes,2,opt,name=ipv4"`
xxx_hidden_Enabled bool `protobuf:"varint,3,opt,name=enabled"`
xxx_hidden_Nick *string `protobuf:"bytes,4,opt,name=nick"`
xxx_hidden_Beacon int64 `protobuf:"varint,5,opt,name=beacon"`
XXX_raceDetectHookData protoimpl.RaceDetectHookData
XXX_presence [1]uint32
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PeerStatus) Reset() {
*x = PeerStatus{}
mi := &file_vpn_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PeerStatus) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PeerStatus) ProtoMessage() {}
func (x *PeerStatus) ProtoReflect() protoreflect.Message {
mi := &file_vpn_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
func (x *PeerStatus) GetPubkey() string {
if x != nil {
if x.xxx_hidden_Pubkey != nil {
return *x.xxx_hidden_Pubkey
}
return ""
}
return ""
}
func (x *PeerStatus) GetIpv4() string {
if x != nil {
if x.xxx_hidden_Ipv4 != nil {
return *x.xxx_hidden_Ipv4
}
return ""
}
return ""
}
func (x *PeerStatus) GetEnabled() bool {
if x != nil {
return x.xxx_hidden_Enabled
}
return false
}
func (x *PeerStatus) GetNick() string {
if x != nil {
if x.xxx_hidden_Nick != nil {
return *x.xxx_hidden_Nick
}
return ""
}
return ""
}
func (x *PeerStatus) GetBeacon() int64 {
if x != nil {
return x.xxx_hidden_Beacon
}
return 0
}
func (x *PeerStatus) SetPubkey(v string) {
x.xxx_hidden_Pubkey = &v
protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 5)
}
func (x *PeerStatus) SetIpv4(v string) {
x.xxx_hidden_Ipv4 = &v
protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 5)
}
func (x *PeerStatus) SetEnabled(v bool) {
x.xxx_hidden_Enabled = v
protoimpl.X.SetPresent(&(x.XXX_presence[0]), 2, 5)
}
func (x *PeerStatus) SetNick(v string) {
x.xxx_hidden_Nick = &v
protoimpl.X.SetPresent(&(x.XXX_presence[0]), 3, 5)
}
func (x *PeerStatus) SetBeacon(v int64) {
x.xxx_hidden_Beacon = v
protoimpl.X.SetPresent(&(x.XXX_presence[0]), 4, 5)
}
func (x *PeerStatus) HasPubkey() bool {
if x == nil {
return false
}
return protoimpl.X.Present(&(x.XXX_presence[0]), 0)
}
func (x *PeerStatus) HasIpv4() bool {
if x == nil {
return false
}
return protoimpl.X.Present(&(x.XXX_presence[0]), 1)
}
func (x *PeerStatus) HasEnabled() bool {
if x == nil {
return false
}
return protoimpl.X.Present(&(x.XXX_presence[0]), 2)
}
func (x *PeerStatus) HasNick() bool {
if x == nil {
return false
}
return protoimpl.X.Present(&(x.XXX_presence[0]), 3)
}
func (x *PeerStatus) HasBeacon() bool {
if x == nil {
return false
}
return protoimpl.X.Present(&(x.XXX_presence[0]), 4)
}
func (x *PeerStatus) ClearPubkey() {
protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0)
x.xxx_hidden_Pubkey = nil
}
func (x *PeerStatus) ClearIpv4() {
protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1)
x.xxx_hidden_Ipv4 = nil
}
func (x *PeerStatus) ClearEnabled() {
protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 2)
x.xxx_hidden_Enabled = false
}
func (x *PeerStatus) ClearNick() {
protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 3)
x.xxx_hidden_Nick = nil
}
func (x *PeerStatus) ClearBeacon() {
protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 4)
x.xxx_hidden_Beacon = 0
}
type PeerStatus_builder struct {
_ [0]func() // Prevents comparability and use of unkeyed literals for the builder.
Pubkey *string
Ipv4 *string
Enabled *bool
Nick *string
Beacon *int64
}
func (b0 PeerStatus_builder) Build() *PeerStatus {
m0 := &PeerStatus{}
b, x := &b0, m0
_, _ = b, x
if b.Pubkey != nil {
protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 5)
x.xxx_hidden_Pubkey = b.Pubkey
}
if b.Ipv4 != nil {
protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 5)
x.xxx_hidden_Ipv4 = b.Ipv4
}
if b.Enabled != nil {
protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 2, 5)
x.xxx_hidden_Enabled = *b.Enabled
}
if b.Nick != nil {
protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 3, 5)
x.xxx_hidden_Nick = b.Nick
}
if b.Beacon != nil {
protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 4, 5)
x.xxx_hidden_Beacon = *b.Beacon
}
return m0
}
type PeerResp struct {
state protoimpl.MessageState `protogen:"opaque.v1"`
xxx_hidden_Peers *[]*PeerStatus `protobuf:"bytes,1,rep,name=peers"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PeerResp) Reset() {
*x = PeerResp{}
mi := &file_vpn_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PeerResp) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PeerResp) ProtoMessage() {}
func (x *PeerResp) ProtoReflect() protoreflect.Message {
mi := &file_vpn_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
func (x *PeerResp) GetPeers() []*PeerStatus {
if x != nil {
if x.xxx_hidden_Peers != nil {
return *x.xxx_hidden_Peers
}
}
return nil
}
func (x *PeerResp) SetPeers(v []*PeerStatus) {
x.xxx_hidden_Peers = &v
}
type PeerResp_builder struct {
_ [0]func() // Prevents comparability and use of unkeyed literals for the builder.
Peers []*PeerStatus
}
func (b0 PeerResp_builder) Build() *PeerResp {
m0 := &PeerResp{}
b, x := &b0, m0
_, _ = b, x
x.xxx_hidden_Peers = &b.Peers
return m0
}
type PeerReq struct {
state protoimpl.MessageState `protogen:"opaque.v1"`
xxx_hidden_Name *string `protobuf:"bytes,1,req,name=name"`
xxx_hidden_Ip4 *string `protobuf:"bytes,2,opt,name=ip4"`
XXX_raceDetectHookData protoimpl.RaceDetectHookData
XXX_presence [1]uint32
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *PeerReq) Reset() {
*x = PeerReq{}
mi := &file_vpn_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *PeerReq) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PeerReq) ProtoMessage() {}
func (x *PeerReq) ProtoReflect() protoreflect.Message {
mi := &file_vpn_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
func (x *PeerReq) GetName() string {
if x != nil {
if x.xxx_hidden_Name != nil {
return *x.xxx_hidden_Name
}
return ""
}
return ""
}
func (x *PeerReq) GetIp4() string {
if x != nil {
if x.xxx_hidden_Ip4 != nil {
return *x.xxx_hidden_Ip4
}
return ""
}
return ""
}
func (x *PeerReq) SetName(v string) {
x.xxx_hidden_Name = &v
protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 2)
}
func (x *PeerReq) SetIp4(v string) {
x.xxx_hidden_Ip4 = &v
protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 2)
}
func (x *PeerReq) HasName() bool {
if x == nil {
return false
}
return protoimpl.X.Present(&(x.XXX_presence[0]), 0)
}
func (x *PeerReq) HasIp4() bool {
if x == nil {
return false
}
return protoimpl.X.Present(&(x.XXX_presence[0]), 1)
}
func (x *PeerReq) ClearName() {
protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0)
x.xxx_hidden_Name = nil
}
func (x *PeerReq) ClearIp4() {
protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1)
x.xxx_hidden_Ip4 = nil
}
type PeerReq_builder struct {
_ [0]func() // Prevents comparability and use of unkeyed literals for the builder.
Name *string
Ip4 *string
}
func (b0 PeerReq_builder) Build() *PeerReq {
m0 := &PeerReq{}
b, x := &b0, m0
_, _ = b, x
if b.Name != nil {
protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 2)
x.xxx_hidden_Name = b.Name
}
if b.Ip4 != nil {
protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 2)
x.xxx_hidden_Ip4 = b.Ip4
}
return m0
}
var File_vpn_proto protoreflect.FileDescriptor
const file_vpn_proto_rawDesc = "" +
"\n" +
"\tvpn.proto\x12\bprotovpn\"~\n" +
"\n" +
"PeerStatus\x12\x16\n" +
"\x06pubkey\x18\x01 \x01(\tR\x06pubkey\x12\x12\n" +
"\x04ipv4\x18\x02 \x01(\tR\x04ipv4\x12\x18\n" +
"\aenabled\x18\x03 \x01(\bR\aenabled\x12\x12\n" +
"\x04nick\x18\x04 \x01(\tR\x04nick\x12\x16\n" +
"\x06beacon\x18\x05 \x01(\x03R\x06beacon\"6\n" +
"\bPeerResp\x12*\n" +
"\x05peers\x18\x01 \x03(\v2\x14.protovpn.PeerStatusR\x05peers\"=\n" +
"\aPeerReq\x12\x19\n" +
"\x04name\x18\x01 \x01(\tB\x05\xaa\x01\x02\b\x03R\x04name\x12\x17\n" +
"\x03ip4\x18\x02 \x01(\tB\x05\xaa\x01\x02\b\x01R\x03ip4B!Z\x1fgit.jasinco.work/wgcl/proto/outb\beditionsp\xe9\a"
var file_vpn_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_vpn_proto_goTypes = []any{
(*PeerStatus)(nil), // 0: protovpn.PeerStatus
(*PeerResp)(nil), // 1: protovpn.PeerResp
(*PeerReq)(nil), // 2: protovpn.PeerReq
}
var file_vpn_proto_depIdxs = []int32{
0, // 0: protovpn.PeerResp.peers:type_name -> protovpn.PeerStatus
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_vpn_proto_init() }
func file_vpn_proto_init() {
if File_vpn_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_vpn_proto_rawDesc), len(file_vpn_proto_rawDesc)),
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_vpn_proto_goTypes,
DependencyIndexes: file_vpn_proto_depIdxs,
MessageInfos: file_vpn_proto_msgTypes,
}.Build()
File_vpn_proto = out.File
file_vpn_proto_goTypes = nil
file_vpn_proto_depIdxs = nil
}

19
proto/vpn.proto Normal file
View file

@ -0,0 +1,19 @@
edition = "2024";
package protovpn;
option go_package = "git.jasinco.work/wgcl/proto/out";
message PeerStatus {
string pubkey = 1;
string ipv4 = 2;
bool enabled = 3;
string nick = 4;
int64 beacon = 5;
}
message PeerResp {
repeated PeerStatus peers = 1;
}
message PeerReq {
string name = 1 [features.field_presence = LEGACY_REQUIRED];
string ip4 = 2 [features.field_presence = EXPLICIT];
}