feat: implement VPN platform management and real protocol services
This commit is contained in:
commit
58c036c34c
45 files changed
+9244
No files matched your search
@@ -0,0 +1,514 @@
|
||||
// Package openvpn manages a real OpenVPN server, its Easy-RSA PKI and dial-in
|
||||
// identities. It never creates a placeholder process when host dependencies
|
||||
// are missing.
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"vpn-platform/backend/internal/core"
|
||||
"vpn-platform/backend/internal/model"
|
||||
"vpn-platform/backend/internal/service"
|
||||
"vpn-platform/backend/internal/store"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
var certificateName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$`)
|
||||
|
||||
func New() *Service { return &Service{} }
|
||||
func (s *Service) Name() string { return "openvpn" }
|
||||
func (s *Service) DisplayName() string { return "OpenVPN" }
|
||||
func (s *Service) Available() (bool, string) {
|
||||
if p, ok := service.LookBin("openvpn"); ok {
|
||||
return true, p
|
||||
}
|
||||
return false, "未检测到 openvpn 二进制,可生成配置但无法启动"
|
||||
}
|
||||
|
||||
func (s *Service) Validate(cfg map[string]interface{}) (map[string]interface{}, error) {
|
||||
out := map[string]interface{}{}
|
||||
for k, v := range cfg {
|
||||
out[k] = v
|
||||
}
|
||||
if service.Num(out, "port", 0) == 0 {
|
||||
out["port"] = 1194
|
||||
}
|
||||
if p := service.Num(out, "port", 0); p < 1 || p > 65535 || p != float64(int(p)) {
|
||||
return nil, fmt.Errorf("port 必须是 1-65535 的整数")
|
||||
}
|
||||
if service.Str(out, "proto") == "" {
|
||||
out["proto"] = "udp"
|
||||
}
|
||||
if p := service.Str(out, "proto"); p != "udp" && p != "tcp-server" {
|
||||
return nil, fmt.Errorf("proto 必须为 udp 或 tcp-server")
|
||||
}
|
||||
if service.Str(out, "subnet") == "" {
|
||||
out["subnet"] = "10.10.0.0 255.255.255.0"
|
||||
}
|
||||
if _, _, err := parseSubnet(service.Str(out, "subnet")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if service.Str(out, "cipher") == "" {
|
||||
out["cipher"] = "AES-256-GCM"
|
||||
}
|
||||
if !regexp.MustCompile(`^[A-Za-z0-9-]+$`).MatchString(service.Str(out, "cipher")) {
|
||||
return nil, fmt.Errorf("cipher 非法")
|
||||
}
|
||||
if service.Str(out, "dns") == "" {
|
||||
out["dns"] = "223.5.5.5"
|
||||
}
|
||||
if net.ParseIP(service.Str(out, "dns")) == nil {
|
||||
return nil, fmt.Errorf("dns 必须是 IP 地址")
|
||||
}
|
||||
if service.Str(out, "auth_mode") == "" {
|
||||
out["auth_mode"] = "certificate"
|
||||
}
|
||||
switch service.Str(out, "auth_mode") {
|
||||
case "certificate", "password", "certificate+password":
|
||||
default:
|
||||
return nil, fmt.Errorf("auth_mode 必须为 certificate、password 或 certificate+password")
|
||||
}
|
||||
if _, ok := out["pki_auto_init"]; !ok {
|
||||
out["pki_auto_init"] = true
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseSubnet(v string) (net.IP, net.IPMask, error) {
|
||||
f := strings.Fields(v)
|
||||
if len(f) != 2 {
|
||||
return nil, nil, fmt.Errorf("subnet 必须为“网络地址 掩码”,例如 10.10.0.0 255.255.255.0")
|
||||
}
|
||||
ip := net.ParseIP(f[0]).To4()
|
||||
mask := net.ParseIP(f[1]).To4()
|
||||
if ip == nil || mask == nil {
|
||||
return nil, nil, fmt.Errorf("subnet 必须是 IPv4 网络")
|
||||
}
|
||||
return ip, net.IPMask(mask), nil
|
||||
}
|
||||
|
||||
func (s *Service) Create(t *model.Tunnel) error {
|
||||
cfg, err := store.TunnelConfig(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := service.WriteConfig(t, "server.conf", s.serverConfig(t, cfg)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := service.WriteConfig(t, "auth-user-pass.sh", authScript(t)); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Chmod(filepath.Join(service.InstanceDir(t), "auth-user-pass.sh"), 0o700)
|
||||
if _, err := service.WriteConfig(t, "init-pki.sh", pkiScript(t)); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Chmod(filepath.Join(service.InstanceDir(t), "init-pki.sh"), 0o700)
|
||||
public, secrets := service.MergeSecrets(cfg)
|
||||
if err := service.SaveTunnelConfig(t, public, secrets); err != nil {
|
||||
return err
|
||||
}
|
||||
// PKI creation is intentionally best-effort: a missing easy-rsa must not be
|
||||
// reported as a running server. Start() verifies every required artifact.
|
||||
_, openvpnInstalled := service.LookBin("openvpn")
|
||||
if service.Bool(cfg, "pki_auto_init", true) && easyRSAPath() != "" && openvpnInstalled && pkiReady(t) != nil {
|
||||
return s.InitializePKI(t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) serverConfig(t *model.Tunnel, cfg map[string]interface{}) string {
|
||||
dir := service.InstanceDir(t)
|
||||
ca, cert, key, dh, ta := filepath.Join(dir, "pki", "ca.crt"), filepath.Join(dir, "pki", "issued", "server.crt"), filepath.Join(dir, "pki", "private", "server.key"), filepath.Join(dir, "pki", "dh.pem"), filepath.Join(dir, "ta.key")
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "port %d\nproto %s\ndev tun\nca %q\ncert %q\nkey %q\ndh %q\ntls-crypt %q\n", int(service.Num(cfg, "port", 1194)), service.StrOr(cfg, "proto", "udp"), ca, cert, key, dh, ta)
|
||||
fmt.Fprintf(&b, "topology subnet\nserver %s\nifconfig-pool-persist %q\npush \"dhcp-option DNS %s\"\npush \"redirect-gateway def1 bypass-dhcp\"\n", service.StrOr(cfg, "subnet", "10.10.0.0 255.255.255.0"), filepath.Join(dir, "ipp.txt"), service.StrOr(cfg, "dns", "223.5.5.5"))
|
||||
fmt.Fprintf(&b, "keepalive 10 120\ndata-ciphers %s:AES-128-GCM\npersist-key\npersist-tun\nstatus %q 10\nstatus-version 3\nverb 3\n", service.StrOr(cfg, "cipher", "AES-256-GCM"), filepath.Join(dir, "openvpn-status.log"))
|
||||
if service.StrOr(cfg, "proto", "udp") == "udp" {
|
||||
b.WriteString("explicit-exit-notify 1\n")
|
||||
}
|
||||
switch service.StrOr(cfg, "auth_mode", "certificate") {
|
||||
case "password":
|
||||
b.WriteString("verify-client-cert none\nusername-as-common-name\n")
|
||||
case "certificate+password":
|
||||
b.WriteString("verify-client-cert require\n")
|
||||
default:
|
||||
b.WriteString("verify-client-cert require\n")
|
||||
}
|
||||
if mode := service.StrOr(cfg, "auth_mode", "certificate"); mode == "password" || mode == "certificate+password" {
|
||||
fmt.Fprintf(&b, "script-security 2\nauth-user-pass-verify %q via-file\n", filepath.Join(dir, "auth-user-pass.sh"))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// InitializePKI creates the CA and server key material in the instance's
|
||||
// private directory. It is safe to call repeatedly after a successful setup.
|
||||
func (s *Service) InitializePKI(t *model.Tunnel) error {
|
||||
if pkiReady(t) == nil {
|
||||
return nil
|
||||
}
|
||||
easy := easyRSAPath()
|
||||
if easy == "" {
|
||||
return fmt.Errorf("无法初始化 OpenVPN PKI:未检测到 easy-rsa/easyrsa")
|
||||
}
|
||||
dir := service.InstanceDir(t)
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runEasyRSA(dir, easy, "init-pki"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runEasyRSA(dir, easy, "--batch", "build-ca", "nopass"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runEasyRSA(dir, easy, "--batch", "build-server-full", "server", "nopass"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runEasyRSA(dir, easy, "gen-dh"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runEasyRSA(dir, easy, "gen-crl"); err != nil {
|
||||
return err
|
||||
}
|
||||
openvpn, ok := service.LookBin("openvpn")
|
||||
if !ok {
|
||||
return fmt.Errorf("PKI 证书已生成,但未检测到 openvpn,无法生成 tls-crypt 密钥")
|
||||
}
|
||||
cmd := exec.Command(openvpn, "--genkey", "secret", filepath.Join(dir, "ta.key"))
|
||||
cmd.Dir = dir
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("生成 tls-crypt 密钥失败: %v: %s", err, out)
|
||||
}
|
||||
return pkiReady(t)
|
||||
}
|
||||
|
||||
func easyRSAPath() string {
|
||||
if p, ok := service.LookBin("easyrsa"); ok {
|
||||
return p
|
||||
}
|
||||
for _, p := range []string{"/usr/share/easy-rsa/easyrsa", "/usr/local/share/easy-rsa/easyrsa"} {
|
||||
if st, err := os.Stat(p); err == nil && st.Mode().IsRegular() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func runEasyRSA(dir, binary string, args ...string) error {
|
||||
cmd := exec.Command(binary, args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "EASYRSA_BATCH=1", "EASYRSA_REQ_CN=OPSX-VPN-CA")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("easy-rsa %s 失败: %v: %s", strings.Join(args, " "), err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func pkiReady(t *model.Tunnel) error {
|
||||
for _, p := range []string{"pki/ca.crt", "pki/issued/server.crt", "pki/private/server.key", "pki/dh.pem", "ta.key"} {
|
||||
if _, err := os.Stat(filepath.Join(service.InstanceDir(t), p)); err != nil {
|
||||
return fmt.Errorf("缺少 %s", p)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func pkiScript(t *model.Tunnel) string {
|
||||
easy := shellQuote(easyRSAPathOrDefault())
|
||||
return "#!/bin/sh\nset -eu\ncd " + shellQuote(service.InstanceDir(t)) + "\nexport EASYRSA_BATCH=1 EASYRSA_REQ_CN=OPSX-VPN-CA\n" + easy + " init-pki\n" + easy + " --batch build-ca nopass\n" + easy + " --batch build-server-full server nopass\n" + easy + " gen-dh\n" + easy + " gen-crl\nopenvpn --genkey secret ta.key\necho 'OpenVPN PKI initialized'\n"
|
||||
}
|
||||
func easyRSAPathOrDefault() string {
|
||||
if p := easyRSAPath(); p != "" {
|
||||
return p
|
||||
}
|
||||
return "easyrsa"
|
||||
}
|
||||
func authScript(t *model.Tunnel) string {
|
||||
exe, _ := os.Executable()
|
||||
if exe == "" {
|
||||
exe = "opsx-vpn"
|
||||
}
|
||||
return "#!/bin/sh\nset -eu\nexec " + shellQuote(exe) + " openvpn-auth --tunnel " + strconv.FormatUint(uint64(t.ID), 10) + " --credentials \"$1\"\n"
|
||||
}
|
||||
func shellQuote(v string) string { return "'" + strings.ReplaceAll(v, "'", "'\\''") + "'" }
|
||||
|
||||
// IssueClientCertificate signs a per-VPNUser certificate and records the exact
|
||||
// certificate CN. LDAP users can use certificates as a second factor too.
|
||||
func (s *Service) IssueClientCertificate(t *model.Tunnel, user *model.VPNUser) error {
|
||||
if user.TunnelID != t.ID {
|
||||
return errors.New("拨号用户不属于该 OpenVPN 隧道")
|
||||
}
|
||||
if !user.Enabled {
|
||||
return errors.New("拨号用户已禁用")
|
||||
}
|
||||
if err := pkiReady(t); err != nil {
|
||||
return fmt.Errorf("无法签发客户端证书:%w", err)
|
||||
}
|
||||
name := user.Certificate
|
||||
if name == "" {
|
||||
name = certificateCN(user.Username)
|
||||
}
|
||||
if !certificateName.MatchString(name) {
|
||||
return fmt.Errorf("客户端证书名称无效")
|
||||
}
|
||||
cert := filepath.Join(service.InstanceDir(t), "pki", "issued", name+".crt")
|
||||
if _, err := os.Stat(cert); err != nil {
|
||||
easy := easyRSAPath()
|
||||
if easy == "" {
|
||||
return fmt.Errorf("未检测到 easy-rsa,无法签发客户端证书")
|
||||
}
|
||||
if err := runEasyRSA(service.InstanceDir(t), easy, "--batch", "build-client-full", name, "nopass"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
user.Certificate = name
|
||||
return store.DB.Save(user).Error
|
||||
}
|
||||
func certificateCN(username string) string {
|
||||
v := strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
|
||||
return r
|
||||
}
|
||||
return '-'
|
||||
}, username)
|
||||
v = strings.Trim(v, "-_")
|
||||
if v == "" {
|
||||
v = "vpn-user"
|
||||
}
|
||||
if len(v) > 48 {
|
||||
v = v[:48]
|
||||
}
|
||||
return "u-" + v
|
||||
}
|
||||
|
||||
// GenerateUserConfig returns an immediately importable profile after signing
|
||||
// the user certificate. It replaces the former instructional, non-functional
|
||||
// profile generation.
|
||||
func (s *Service) GenerateUserConfig(t *model.Tunnel, user *model.VPNUser) (string, error) {
|
||||
cfg, err := store.TunnelConfig(t)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
mode := service.StrOr(cfg, "auth_mode", "certificate")
|
||||
if mode != "password" {
|
||||
if err := s.IssueClientCertificate(t, user); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
dir := service.InstanceDir(t)
|
||||
read := func(p string) (string, error) { b, e := os.ReadFile(p); return string(b), e }
|
||||
ca, e := read(filepath.Join(dir, "pki", "ca.crt"))
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
ta, e := read(filepath.Join(dir, "ta.key"))
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
var cert, key string
|
||||
if mode != "password" {
|
||||
cert, e = read(filepath.Join(dir, "pki", "issued", user.Certificate+".crt"))
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
key, e = read(filepath.Join(dir, "pki", "private", user.Certificate+".key"))
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
}
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "client\ndev tun\nproto %s\nremote %s %d\nresolv-retry infinite\nnobind\npersist-key\npersist-tun\nremote-cert-tls server\ndata-ciphers %s:AES-128-GCM\nverb 3\n", clientProto(service.StrOr(cfg, "proto", "udp")), service.StrOr(cfg, "public_endpoint", "your-server-ip"), int(service.Num(cfg, "port", 1194)), service.StrOr(cfg, "cipher", "AES-256-GCM"))
|
||||
if mode == "password" || mode == "certificate+password" {
|
||||
b.WriteString("auth-user-pass\n")
|
||||
}
|
||||
b.WriteString("<ca>\n" + ca + "</ca>\n")
|
||||
if cert != "" {
|
||||
b.WriteString("<cert>\n" + cert + "</cert>\n<key>\n" + key + "</key>\n")
|
||||
}
|
||||
b.WriteString("<tls-crypt>\n" + ta + "</tls-crypt>\n")
|
||||
return b.String(), nil
|
||||
}
|
||||
func clientProto(proto string) string {
|
||||
if proto == "tcp-server" {
|
||||
return "tcp-client"
|
||||
}
|
||||
return proto
|
||||
}
|
||||
|
||||
func (s *Service) GenerateClientConfig(t *model.Tunnel) (string, error) {
|
||||
cfg, err := store.TunnelConfig(t)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
username := service.Str(cfg, "client_username")
|
||||
if username == "" {
|
||||
return "", fmt.Errorf("请指定 client_username 或通过拨号用户下载其专属 .ovpn 配置")
|
||||
}
|
||||
var user model.VPNUser
|
||||
if err := store.DB.Where("tunnel_id = ? AND username = ?", t.ID, username).First(&user).Error; err != nil {
|
||||
return "", fmt.Errorf("拨号用户 %q 不存在: %w", username, err)
|
||||
}
|
||||
return s.GenerateUserConfig(t, &user)
|
||||
}
|
||||
|
||||
func (s *Service) Start(t *model.Tunnel) error {
|
||||
if _, ok := service.LookBin("openvpn"); !ok {
|
||||
return fmt.Errorf("无法启动 OpenVPN:未检测到 openvpn")
|
||||
}
|
||||
if err := pkiReady(t); err != nil {
|
||||
return fmt.Errorf("无法启动 OpenVPN:PKI 尚未就绪:%w", err)
|
||||
}
|
||||
return service.StartDaemon(t, serviceMustBin("openvpn"), "--config", filepath.Join(service.InstanceDir(t), "server.conf"))
|
||||
}
|
||||
func serviceMustBin(name string) string { p, _ := service.LookBin(name); return p }
|
||||
func (s *Service) Stop(t *model.Tunnel) error { return service.KillPid(t) }
|
||||
func (s *Service) Status(t *model.Tunnel) (core.Status, error) {
|
||||
if _, alive := service.ReadPid(t); alive {
|
||||
st := service.NowStatus(true, "running", "OpenVPN 进程运行中")
|
||||
rx, tx := s.collectTraffic(t)
|
||||
st.RxBytes = t.RxBytes + rx
|
||||
st.TxBytes = t.TxBytes + tx
|
||||
return st, nil
|
||||
}
|
||||
if _, ok := service.LookBin("openvpn"); !ok {
|
||||
return service.NowStatus(false, "undeployed", "配置已生成,但未检测到 openvpn,尚未部署"), nil
|
||||
}
|
||||
return service.NowStatus(false, "stopped", "未启动"), nil
|
||||
}
|
||||
|
||||
type trafficSnapshot map[string]trafficCounter
|
||||
type trafficCounter struct {
|
||||
Rx int64 `json:"rx"`
|
||||
Tx int64 `json:"tx"`
|
||||
}
|
||||
|
||||
func (s *Service) collectTraffic(t *model.Tunnel) (int64, int64) {
|
||||
raw, err := os.ReadFile(filepath.Join(service.InstanceDir(t), "openvpn-status.log"))
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
snap := trafficSnapshot{}
|
||||
if b, e := os.ReadFile(filepath.Join(service.InstanceDir(t), "traffic-state.json")); e == nil {
|
||||
_ = json.Unmarshal(b, &snap)
|
||||
}
|
||||
next := trafficSnapshot{}
|
||||
var totalRx, totalTx int64
|
||||
now := time.Now()
|
||||
for _, line := range strings.Split(string(raw), "\n") {
|
||||
f := strings.Split(line, ",")
|
||||
if len(f) < 7 || f[0] != "CLIENT_LIST" {
|
||||
continue
|
||||
}
|
||||
rx, e1 := strconv.ParseInt(f[5], 10, 64)
|
||||
tx, e2 := strconv.ParseInt(f[6], 10, 64)
|
||||
if e1 != nil || e2 != nil {
|
||||
continue
|
||||
}
|
||||
cn := f[1]
|
||||
username := cn
|
||||
if len(f) > 9 && f[9] != "" && f[9] != "UNDEF" {
|
||||
username = f[9]
|
||||
}
|
||||
key := username + "|" + f[2]
|
||||
old := snap[key]
|
||||
deltaRx, deltaTx := rx-old.Rx, tx-old.Tx
|
||||
if deltaRx < 0 {
|
||||
deltaRx = rx
|
||||
}
|
||||
if deltaTx < 0 {
|
||||
deltaTx = tx
|
||||
}
|
||||
totalRx += deltaRx
|
||||
totalTx += deltaTx
|
||||
next[key] = trafficCounter{rx, tx}
|
||||
if store.DB != nil && username != "" {
|
||||
store.DB.Model(&model.VPNUser{}).Where("tunnel_id = ? AND (username = ? OR certificate = ?)", t.ID, username, cn).Updates(map[string]interface{}{"rx_bytes": gorm.Expr("rx_bytes + ?", deltaRx), "tx_bytes": gorm.Expr("tx_bytes + ?", deltaTx), "last_seen_at": now})
|
||||
}
|
||||
}
|
||||
b, _ := json.Marshal(next)
|
||||
_ = os.WriteFile(filepath.Join(service.InstanceDir(t), "traffic-state.json"), b, 0o600)
|
||||
return totalRx, totalTx
|
||||
}
|
||||
func (s *Service) Delete(t *model.Tunnel) error { return s.Stop(t) }
|
||||
|
||||
// RunAuthHelper is called by the same opsx-vpn binary from OpenVPN's
|
||||
// auth-user-pass-verify hook. It returns 0 only for enabled, tunnel-scoped
|
||||
// local users or LDAP users whose recorded LDAP DN accepts the password.
|
||||
func RunAuthHelper(args []string) error {
|
||||
fs := flag.NewFlagSet("openvpn-auth", flag.ContinueOnError)
|
||||
fs.SetOutput(os.Stderr)
|
||||
tunnel := fs.Uint("tunnel", 0, "tunnel id")
|
||||
credentials := fs.String("credentials", "", "credentials file")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *tunnel == 0 || *credentials == "" {
|
||||
return errors.New("--tunnel 和 --credentials 必填")
|
||||
}
|
||||
raw, err := os.ReadFile(*credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lines := strings.Split(strings.ReplaceAll(string(raw), "\r\n", "\n"), "\n")
|
||||
if len(lines) < 2 || strings.TrimSpace(lines[0]) == "" {
|
||||
return errors.New("认证凭据格式无效")
|
||||
}
|
||||
return Authenticate(uint(*tunnel), strings.TrimSpace(lines[0]), strings.TrimSpace(lines[1]))
|
||||
}
|
||||
func Authenticate(tunnelID uint, username, password string) error {
|
||||
if password == "" {
|
||||
return errors.New("密码为空")
|
||||
}
|
||||
var user model.VPNUser
|
||||
if err := store.DB.Where("tunnel_id = ? AND username = ? AND enabled = ?", tunnelID, username, true).First(&user).Error; err != nil {
|
||||
return errors.New("拨号用户不存在或已禁用")
|
||||
}
|
||||
if user.Source == "local" {
|
||||
if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) != nil {
|
||||
return errors.New("密码错误")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if user.Source != "ldap" || user.LDAPDN == "" {
|
||||
return errors.New("拨号用户认证来源无效")
|
||||
}
|
||||
var cfg model.LDAPConfig
|
||||
if err := store.DB.Where("enabled = ?", true).First(&cfg).Error; err != nil {
|
||||
return errors.New("LDAP 未启用")
|
||||
}
|
||||
url := cfg.URL
|
||||
if !strings.HasPrefix(url, "ldap://") && !strings.HasPrefix(url, "ldaps://") {
|
||||
url = "ldap://" + url
|
||||
}
|
||||
conn, err := ldap.DialURL(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
if cfg.BindDN != "" {
|
||||
bind, err := store.DecryptSecret(cfg.BindPassword)
|
||||
if err != nil {
|
||||
return errors.New("LDAP 绑定密钥无效")
|
||||
}
|
||||
if err := conn.Bind(cfg.BindDN, bind); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := conn.Bind(user.LDAPDN, password); err != nil {
|
||||
return errors.New("LDAP 密码错误")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user