500 lines
18 KiB
Go
500 lines
18 KiB
Go
package openvpn
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/glebarez/sqlite"
|
|
"github.com/pquerna/otp/totp"
|
|
"gorm.io/gorm"
|
|
|
|
"vpn-platform/backend/internal/model"
|
|
"vpn-platform/backend/internal/service"
|
|
"vpn-platform/backend/internal/store"
|
|
)
|
|
|
|
func TestOIDCConfigAndSecrets(t *testing.T) {
|
|
t.Setenv("VPN_DATA_DIR", t.TempDir())
|
|
tunnel := &model.Tunnel{ID: 42, Protocol: "openvpn"}
|
|
input := map[string]interface{}{
|
|
"auth_mode": "oidc", "oidc_issuer": "https://idp.example.com/realms/team",
|
|
"oidc_base_url": "https://vpn-login.example.com", "oidc_client_id": "vpn-client",
|
|
"oidc_client_secret": "secret-\nadmin: true", "oidc_http_port": 19242,
|
|
}
|
|
cfg, err := New().Validate(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := prepareOIDC(tunnel, cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conf := New().serverConfig(tunnel, cfg)
|
|
for _, required := range []string{"management-client-auth", "auth-user-pass-optional", "verify-client-cert none"} {
|
|
if !strings.Contains(conf, required) {
|
|
t.Fatalf("OIDC server config missing %q", required)
|
|
}
|
|
}
|
|
if strings.Contains(conf, "auth-user-pass-verify") || strings.Contains(conf, "secret-") {
|
|
t.Fatal("OIDC server config must not use password helper or leak secrets")
|
|
}
|
|
path := filepath.Join(service.InstanceDir(tunnel), "openvpn-auth-oauth2.yaml")
|
|
bytes, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(bytes), `secret: "secret-\nadmin: true"`) || strings.Contains(string(bytes), "\nadmin: true\n") {
|
|
t.Fatal("OIDC client secret must remain one quoted YAML value")
|
|
}
|
|
if stat, err := os.Stat(path); err != nil || stat.Mode().Perm() != 0o600 {
|
|
t.Fatalf("auth configuration must be 0600: %v %v", stat, err)
|
|
}
|
|
pagePath := filepath.Join(service.InstanceDir(tunnel), "auth-result.html")
|
|
page, err := os.ReadFile(pagePath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stat, err := os.Stat(pagePath); err != nil || stat.Mode().Perm() != 0o600 {
|
|
t.Fatalf("auth result template must be 0600: %v %v", stat, err)
|
|
}
|
|
for _, want := range []string{"name=\"viewport\"", "请返回 VPN 客户端", "请在 VPN 客户端查看连接状态"} {
|
|
if !strings.Contains(string(page), want) {
|
|
t.Fatalf("auth result template missing %q", want)
|
|
}
|
|
}
|
|
passwordPath := filepath.Join(service.InstanceDir(tunnel), "management-password.txt")
|
|
if stat, err := os.Stat(passwordPath); err != nil || stat.Mode().Perm() != 0o600 {
|
|
t.Fatalf("management password must be 0600: %v %v", stat, err)
|
|
}
|
|
public, secrets := service.MergeSecrets(cfg, "oidc_client_secret", "oidc_http_secret", "oidc_management_password")
|
|
if public["oidc_client_secret"] != nil || secrets["oidc_client_secret"] == nil {
|
|
t.Fatal("OIDC credentials must not be kept in public config")
|
|
}
|
|
tunnel.Config = service.ConfigJSON(public)
|
|
dir := service.InstanceDir(tunnel)
|
|
if err := os.MkdirAll(filepath.Join(dir, "pki"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for path, content := range map[string]string{"pki/ca.crt": "test-ca\n", "ta.key": "test-tls-key\n"} {
|
|
if err := os.WriteFile(filepath.Join(dir, path), []byte(content), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
client, err := New().GenerateClientConfig(tunnel)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(client, "auth-user-pass") || strings.Contains(client, "<cert>") || !strings.Contains(client, "<tls-crypt>") {
|
|
t.Fatal("OIDC client profile must use WebAuth without password or client certificate")
|
|
}
|
|
}
|
|
|
|
func TestBuiltInOIDCUsesStableSubjectAsOpenVPNUsername(t *testing.T) {
|
|
tunnel := &model.Tunnel{ID: 44, Protocol: "openvpn"}
|
|
config := map[string]interface{}{
|
|
"oidc_provider": "builtin", "oidc_issuer": "https://vpn.example.com/id",
|
|
}
|
|
generated := oidcConfig(tunnel, config, true)
|
|
if !strings.Contains(generated, `openvpn-username: "string(token.claims.sub)"`) {
|
|
t.Fatalf("built-in issuer must use verified subject as username: %s", generated)
|
|
}
|
|
if !strings.Contains(generated, "override-username: true") {
|
|
t.Fatal("OpenVPN 2.7 must receive the authenticated subject in its status output")
|
|
}
|
|
if generated := oidcConfig(tunnel, config, false); strings.Contains(generated, "override-username:") {
|
|
t.Fatal("OpenVPN 2.6 must not receive an unsupported username override")
|
|
}
|
|
config["oidc_provider"] = "external"
|
|
if generated := oidcConfig(tunnel, config, true); strings.Contains(generated, "openvpn-username:") || strings.Contains(generated, "override-username:") {
|
|
t.Fatalf("external issuer username behavior must remain unchanged: %s", generated)
|
|
}
|
|
}
|
|
|
|
func TestOverrideUsernameRequiresOpenVPN27(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("VPN_DATA_DIR", dir)
|
|
binDir := filepath.Join(dir, "bin")
|
|
if err := os.MkdirAll(binDir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bin := filepath.Join(binDir, "openvpn")
|
|
for _, tc := range []struct {
|
|
version string
|
|
want bool
|
|
}{
|
|
{"OpenVPN 2.6.19 test\n", false},
|
|
{"OpenVPN 2.7.8 test\n", true},
|
|
{"unexpected output\n", false},
|
|
} {
|
|
if err := os.WriteFile(bin, []byte("#!/bin/sh\necho '"+strings.TrimSpace(tc.version)+"'\n"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := supportsOverrideUsername(); got != tc.want {
|
|
t.Fatalf("version %q: supportsOverrideUsername() = %v, want %v", tc.version, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOIDCRejectsUnsafeURLs(t *testing.T) {
|
|
base := map[string]interface{}{
|
|
"auth_mode": "oidc", "oidc_issuer": "https://idp.example.com/realms/a",
|
|
"oidc_base_url": "https://login.example.com", "oidc_client_id": "vpn",
|
|
}
|
|
for _, value := range []string{"https://login.example.com/extra", "https://user@login.example.com", "https://login.example.com/?code=abc", "ftp://login.example.com"} {
|
|
cfg := service.CloneConfig(base)
|
|
cfg["oidc_base_url"] = value
|
|
if _, err := New().Validate(cfg); err == nil {
|
|
t.Errorf("expected invalid OIDC callback base URL: %q", value)
|
|
}
|
|
}
|
|
if _, err := New().Validate(base); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
httpBase := service.CloneConfig(base)
|
|
httpBase["oidc_base_url"] = "http://login.example.com:8080"
|
|
httpBase["oidc_issuer"] = "http://login.example.com:8080/id"
|
|
if _, err := New().Validate(httpBase); err != nil {
|
|
t.Fatalf("http public base must be allowed: %v", err)
|
|
}
|
|
base["auth_mode"] = "password"
|
|
base["oidc_client_secret"] = "must-not-survive"
|
|
cfg, err := New().Validate(base)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := cfg["oidc_client_secret"]; exists {
|
|
t.Fatal("switching away from OIDC must remove its credentials")
|
|
}
|
|
}
|
|
|
|
func TestSwitchingAwayFromBuiltInCannotReuseItsClientSecret(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("VPN_DATA_DIR", dir)
|
|
if err := store.Init(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secret, err := store.EncryptSecret(`{"oidc_client_secret":"old-built-in-secret"}`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tunnel := &model.Tunnel{ID: 43, Protocol: "openvpn", Config: `{"oidc_provider":"builtin"}`, Secrets: secret}
|
|
cfg := map[string]interface{}{
|
|
"oidc_provider": "external", "oidc_issuer": "https://external.example.com/id",
|
|
"oidc_base_url": "https://login.example.com", "oidc_client_id": "external-client",
|
|
}
|
|
if err := prepareOIDC(tunnel, cfg); err == nil {
|
|
t.Fatal("external provider reused a built-in client secret")
|
|
}
|
|
cfg["oidc_provider"] = "builtin"
|
|
cfg["oidc_client_secret"] = "forged"
|
|
if err := prepareOIDC(tunnel, cfg); err == nil {
|
|
t.Fatal("built-in provider accepted an unregistered issuer and client")
|
|
}
|
|
}
|
|
|
|
func TestOIDCRequiresOpenVPN262(t *testing.T) {
|
|
for _, version := range []string{"OpenVPN 2.5.11 x86_64", "OpenVPN 2.6.1 x86_64", "unknown"} {
|
|
if err := validateOIDCOpenVPNVersion([]byte(version)); err == nil {
|
|
t.Errorf("expected %q to fail minimum version check", version)
|
|
}
|
|
}
|
|
for _, version := range []string{"OpenVPN 2.6.2 x86_64", "OpenVPN 2.7.0 x86_64", "OpenVPN 3.0.0 x86_64"} {
|
|
if err := validateOIDCOpenVPNVersion([]byte(version)); err != nil {
|
|
t.Errorf("expected %q to pass: %v", version, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOIDCTunnelsNeedDistinctCallbackAndPort(t *testing.T) {
|
|
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "test.db")), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldDB := store.DB
|
|
store.DB = db
|
|
t.Cleanup(func() { store.DB = oldDB })
|
|
if err := db.AutoMigrate(&model.Tunnel{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first := model.Tunnel{Name: "first", Protocol: "openvpn", Config: service.ConfigJSON(map[string]interface{}{
|
|
"auth_mode": "oidc", "oidc_base_url": "https://login.example.com", "oidc_http_port": 19090,
|
|
})}
|
|
if err := db.Create(&first).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other := &model.Tunnel{ID: first.ID + 1, Protocol: "openvpn"}
|
|
for _, cfg := range []map[string]interface{}{
|
|
{"oidc_base_url": "https://login.example.com/", "oidc_http_port": 19091},
|
|
{"oidc_base_url": "https://another.example.com", "oidc_http_port": 19090},
|
|
} {
|
|
if err := checkOIDCConflicts(other, cfg); err == nil {
|
|
t.Errorf("expected conflict: %#v", cfg)
|
|
}
|
|
}
|
|
if err := checkOIDCConflicts(other, map[string]interface{}{"oidc_base_url": "https://another.example.com", "oidc_http_port": 19091}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestValidateOpenVPNModes(t *testing.T) {
|
|
feishu := map[string]interface{}{"auth_mode": "oidc", "oidc_provider": "builtin", "oidc_issuer": "https://vpn.example.com/id", "oidc_base_url": "https://vpn.example.com", "oidc_client_id": "opsx-vpn", "feishu_access_mode": "allowlist"}
|
|
if _, err := New().Validate(feishu); err != nil {
|
|
t.Fatalf("valid member policy: %v", err)
|
|
}
|
|
feishu["feishu_access_mode"] = true
|
|
if _, err := New().Validate(feishu); err == nil {
|
|
t.Fatal("non-string member policy accepted")
|
|
}
|
|
feishu["feishu_access_mode"] = "allowlist"
|
|
feishu["oidc_provider"] = "external"
|
|
if _, err := New().Validate(feishu); err == nil {
|
|
t.Fatal("external OIDC accepted Feishu member policy")
|
|
}
|
|
cfg, err := New().Validate(map[string]interface{}{"auth_mode": "password", "subnet": "10.8.0.0 255.255.255.0"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg["proto"] != "tcp-server" || cfg["port"] != 1194 || cfg["auth_mode"] != "password" {
|
|
t.Fatalf("defaults not applied: %#v", cfg)
|
|
}
|
|
if cfg["enable_nat"] != true {
|
|
t.Fatalf("NAT should default to enabled: %#v", cfg)
|
|
}
|
|
if _, err := New().Validate(map[string]interface{}{"auth_mode": "unknown"}); err == nil {
|
|
t.Fatal("expected invalid auth mode error")
|
|
}
|
|
// 前端历史值 tcp 应归一为 tcp-server
|
|
cfg2, err := New().Validate(map[string]interface{}{"proto": "tcp", "subnet": "10.8.0.0 255.255.255.0"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg2["proto"] != "tcp-server" {
|
|
t.Fatalf("tcp not normalized: %#v", cfg2)
|
|
}
|
|
cfg3, err := New().Validate(map[string]interface{}{
|
|
"subnet": "10.8.0.0 255.255.255.0", "push_routes": "# VPC\n192.168.1.0/24\n# staging\n10.0.0.10",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg3["redirect_gateway"] != false {
|
|
t.Fatalf("push_routes should default redirect_gateway off: %#v", cfg3)
|
|
}
|
|
if cfg3["push_routes"] != "# VPC\n192.168.1.0/24\n# staging\n10.0.0.10/32" {
|
|
t.Fatalf("push_routes not normalized: %#v", cfg3)
|
|
}
|
|
if _, err := New().Validate(map[string]interface{}{"subnet": "10.8.0.0 255.255.255.0", "push_routes": "10.0.0.999"}); err == nil {
|
|
t.Fatal("expected invalid route to fail")
|
|
}
|
|
// MFA 仅允许密码相关模式
|
|
if _, err := New().Validate(map[string]interface{}{
|
|
"auth_mode": "certificate", "mfa_mode": "required", "subnet": "10.8.0.0 255.255.255.0",
|
|
}); err == nil {
|
|
t.Fatal("expected MFA + certificate-only to fail")
|
|
}
|
|
cfg4, err := New().Validate(map[string]interface{}{
|
|
"auth_mode": "password", "mfa_mode": "required", "subnet": "10.8.0.0 255.255.255.0", "proto": "tcp",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg4["proto"] != "tcp-server" || cfg4["mfa_mode"] != "required" {
|
|
t.Fatalf("mfa/tcp normalize: %#v", cfg4)
|
|
}
|
|
}
|
|
|
|
func TestSplitPasswordAndOTP(t *testing.T) {
|
|
pw, otp := splitPasswordAndOTP("secretpass123456", "")
|
|
if pw != "secretpass" || otp != "123456" {
|
|
t.Fatalf("suffix: %q %q", pw, otp)
|
|
}
|
|
scrv := "SCRV1:" + base64.StdEncoding.EncodeToString([]byte("mypass")) + ":" + base64.StdEncoding.EncodeToString([]byte("654321"))
|
|
pw, otp = splitPasswordAndOTP(scrv, "")
|
|
if pw != "mypass" || otp != "654321" {
|
|
t.Fatalf("scrv1: %q %q", pw, otp)
|
|
}
|
|
pw, otp = splitPasswordAndOTP("onlypass", "111222")
|
|
if pw != "onlypass" || otp != "111222" {
|
|
t.Fatalf("challenge line: %q %q", pw, otp)
|
|
}
|
|
}
|
|
|
|
func TestClientProto(t *testing.T) {
|
|
if clientProto("tcp-server") != "tcp-client" || clientProto("udp") != "udp" || clientProto("tcp") != "tcp-client" {
|
|
t.Fatal("clientProto mapping failed")
|
|
}
|
|
}
|
|
|
|
func TestServerConfigIncludesProductionTunings(t *testing.T) {
|
|
cfg, err := New().Validate(map[string]interface{}{
|
|
"auth_mode": "password", "proto": "tcp", "subnet": "10.8.0.0 255.255.240.0",
|
|
"max_clients": 1000, "mfa_mode": "off",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg["proto"] != "tcp-server" {
|
|
t.Fatalf("proto: %#v", cfg["proto"])
|
|
}
|
|
tunn := &model.Tunnel{ID: 1, Protocol: "openvpn"}
|
|
raw := New().serverConfig(tunn, cfg)
|
|
for _, want := range []string{
|
|
"proto tcp-server",
|
|
"tun-mtu 1500",
|
|
"mssfix 1450",
|
|
"keepalive 5 30",
|
|
"reneg-sec 0",
|
|
"verify-client-cert none",
|
|
"username-as-common-name",
|
|
"max-clients 1000",
|
|
"duplicate-cn",
|
|
"via-file",
|
|
"data-ciphers-fallback AES-256-GCM",
|
|
} {
|
|
if !strings.Contains(raw, want) {
|
|
t.Fatalf("server.conf missing %q\n%s", want, raw)
|
|
}
|
|
}
|
|
if strings.Contains(raw, "via-env") {
|
|
t.Fatal("should use via-file not via-env")
|
|
}
|
|
for _, obsolete := range []string{"persist-key", "ifconfig-pool-persist"} {
|
|
if strings.Contains(raw, obsolete) {
|
|
t.Fatalf("server.conf should not contain obsolete %q\n%s", obsolete, raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateTOTPSkew(t *testing.T) {
|
|
// 使用固定 secret 生成当前码并校验
|
|
secret := "JBSWY3DPEHPK3PXP" // "Hello!" base32, common test secret
|
|
code, err := totp.GenerateCode(secret, time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ok, counter := validateTOTPSkew(code, secret, 1)
|
|
if !ok || counter == 0 {
|
|
t.Fatalf("expected valid totp, ok=%v counter=%d", ok, counter)
|
|
}
|
|
if ok2, _ := validateTOTPSkew("000000", secret, 1); ok2 {
|
|
t.Fatal("expected invalid code")
|
|
}
|
|
}
|
|
|
|
func TestCertificateCNIsSafeAndStable(t *testing.T) {
|
|
cn := certificateCN("张三 / alice@example.com")
|
|
if !certificateName.MatchString(cn) || !strings.HasPrefix(cn, "u-") {
|
|
t.Fatalf("unsafe CN: %q", cn)
|
|
}
|
|
}
|
|
|
|
func TestPKIScriptUses99YearCertificates(t *testing.T) {
|
|
script := pkiScript(&model.Tunnel{ID: 1, Protocol: "openvpn"})
|
|
for _, want := range []string{"EASYRSA_CA_EXPIRE=36135", "EASYRSA_CERT_EXPIRE=36135"} {
|
|
if !strings.Contains(script, want) {
|
|
t.Fatalf("PKI script missing %q\n%s", want, script)
|
|
}
|
|
}
|
|
if !strings.Contains(script, "EASYRSA_REQ_CN=OPSX-VPN-CA") {
|
|
t.Fatalf("PKI script should set a CA name: %s", script)
|
|
}
|
|
if strings.Contains(script, "export EASYRSA_BATCH=1 EASYRSA_REQ_CN") {
|
|
t.Fatalf("PKI script must not export the CA CN to server/client certificate commands: %s", script)
|
|
}
|
|
}
|
|
|
|
func TestSplitStatusFieldsSupportsOpenVPNStatusV3(t *testing.T) {
|
|
line := "CLIENT_LIST\tadmin\ttcp4-server:203.0.113.1:1194\t10.8.0.2\t\t7161451\t8489433\t2026-07-25 23:32:58\t1784993578\tadmin"
|
|
fields := splitStatusFields(line)
|
|
if len(fields) != 10 || fields[5] != "7161451" || fields[9] != "admin" {
|
|
t.Fatalf("status-version 3 fields parsed incorrectly: %#v", fields)
|
|
}
|
|
}
|
|
|
|
func TestStatusUsernameDoesNotTreatUNDEFAsIdentity(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, cn, username, want string
|
|
}{
|
|
{"password", "admin", "admin", "admin"},
|
|
{"authenticated username", "UNDEF", "tenant:ou_123", "tenant:ou_123"},
|
|
{"certificate", "client-cert", "UNDEF", "client-cert"},
|
|
{"webauth without username", "UNDEF", "UNDEF", ""},
|
|
{"webauth empty username", "UNDEF", "", ""},
|
|
{"webauth lowercase placeholder", "undef", "UnDeF", ""},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
fields := splitStatusFields("CLIENT_LIST\t" + tc.cn + "\t203.0.113.1:1234\t10.8.0.2\t\t12\t34\t2026-10-22\t1234\t" + tc.username)
|
|
if got := statusUsername(fields); got != tc.want {
|
|
t.Fatalf("statusUsername() = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWebAuthSessionIdentityAndTrafficTransition(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("VPN_DATA_DIR", dir)
|
|
previousDB := store.DB
|
|
t.Cleanup(func() {
|
|
if store.DB != nil {
|
|
if sqlDB, err := store.DB.DB(); err == nil {
|
|
_ = sqlDB.Close()
|
|
}
|
|
}
|
|
store.DB = previousDB
|
|
})
|
|
if err := store.Init(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tunnel := &model.Tunnel{ID: 502, Name: "webauth-test", Protocol: "openvpn"}
|
|
if err := store.DB.Create(tunnel).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, username := range []string{"UNDEF", "tenant:ou_123"} {
|
|
if err := store.DB.Create(&model.VPNUser{TunnelID: tunnel.ID, Username: username, Enabled: true}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
instanceDir := service.InstanceDir(tunnel)
|
|
if err := os.MkdirAll(instanceDir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeStatus := func(username string, rx, tx int64) {
|
|
t.Helper()
|
|
line := fmt.Sprintf("CLIENT_LIST\tUNDEF\t203.0.113.1:1234\t10.8.0.2\t\t%d\t%d\t2026-10-22\t1234\t%s\n", rx, tx, username)
|
|
if err := os.WriteFile(filepath.Join(instanceDir, "openvpn-status.log"), []byte(line), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
writeStatus("UNDEF", 150, 230)
|
|
svc := New()
|
|
if rx, tx := svc.collectTraffic(tunnel); rx != 150 || tx != 230 {
|
|
t.Fatalf("initial traffic = %d/%d", rx, tx)
|
|
}
|
|
var unknownCount int64
|
|
store.DB.Model(&model.VPNSession{}).Where("tunnel_id = ?", tunnel.ID).Count(&unknownCount)
|
|
if unknownCount != 0 {
|
|
t.Fatalf("pre-auth socket created %d audit sessions", unknownCount)
|
|
}
|
|
var placeholder model.VPNUser
|
|
store.DB.Where("tunnel_id = ? AND username = ?", tunnel.ID, "UNDEF").First(&placeholder)
|
|
if placeholder.RxBytes != 0 || placeholder.TxBytes != 0 {
|
|
t.Fatal("UNDEF placeholder received VPN user traffic")
|
|
}
|
|
if rx, tx := svc.collectTraffic(tunnel); rx != 0 || tx != 0 {
|
|
t.Fatalf("unchanged snapshot counted twice: %d/%d", rx, tx)
|
|
}
|
|
writeStatus("tenant:ou_123", 160, 260)
|
|
if rx, tx := svc.collectTraffic(tunnel); rx != 10 || tx != 30 {
|
|
t.Fatalf("authenticated snapshot delta = %d/%d, want 10/30", rx, tx)
|
|
}
|
|
var identified model.VPNSession
|
|
if err := store.DB.Where("tunnel_id = ? AND status = ?", tunnel.ID, "active").First(&identified).Error; err != nil || identified.Username != "tenant:ou_123" || identified.VPNUserID == 0 {
|
|
t.Fatalf("verified username should link its own user: %+v, %v", identified, err)
|
|
}
|
|
}
|