- 新增日志中心功能 (api/logapi.go + Logs.vue) - 新增GRE协议服务支持 (service/gre/) - 新增流量监控组件 (TrafficChart.vue) - 新增L2TP/IPSec测试用例 - 新增日志轮转服务 (logrotate.go) - 新增部署脚本 (scripts/install.sh, uninstall.sh, service文件) - 更新i18n国际化文件 - 优化后端API和前端路由
181 lines
5.0 KiB
Go
181 lines
5.0 KiB
Go
package l2tp
|
|
|
|
import (
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
|
|
"vpn-platform/backend/internal/model"
|
|
"vpn-platform/backend/internal/service"
|
|
"vpn-platform/backend/internal/store"
|
|
)
|
|
|
|
func TestValidateDefaults(t *testing.T) {
|
|
cfg, err := New().Validate(map[string]interface{}{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defaults := []struct {
|
|
key string
|
|
expected string
|
|
}{
|
|
{"ip_range", "10.9.0.100-10.9.0.200"},
|
|
{"local_ip", "10.9.0.1"},
|
|
{"ike_proposal", "aes256-sha256-modp2048,aes128-sha1-modp1024"},
|
|
{"esp_proposal", "aes256-sha256,aes128-sha1"},
|
|
}
|
|
for _, d := range defaults {
|
|
if got := service.Str(cfg, d.key); got != d.expected {
|
|
t.Errorf("%s: got %q, want %q", d.key, got, d.expected)
|
|
}
|
|
}
|
|
if service.Str(cfg, "dns") == "" {
|
|
t.Error("dns default should not be empty")
|
|
}
|
|
if service.Str(cfg, "client_subnet") == "" {
|
|
t.Error("client_subnet default should not be empty")
|
|
}
|
|
// Verify client_subnet covers the ip_range
|
|
prefix, err := netip.ParsePrefix(service.Str(cfg, "client_subnet"))
|
|
if err != nil {
|
|
t.Fatalf("client_subnet parse: %v", err)
|
|
}
|
|
start, _, _ := parseIPRange(service.Str(cfg, "ip_range"))
|
|
if !prefix.Contains(start) {
|
|
t.Errorf("client_subnet %s does not contain start of ip_range %s", prefix, start)
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsInvalid(t *testing.T) {
|
|
s := New()
|
|
cases := []struct {
|
|
name string
|
|
cfg map[string]interface{}
|
|
}{
|
|
{"invalid local_ip", map[string]interface{}{
|
|
"local_ip": "not-an-ip",
|
|
}},
|
|
{"invalid ip_range format", map[string]interface{}{
|
|
"ip_range": "10.0.0.100",
|
|
}},
|
|
{"ip_range start > end", map[string]interface{}{
|
|
"ip_range": "10.9.0.200-10.9.0.100",
|
|
}},
|
|
{"local_ip in ip_range", map[string]interface{}{
|
|
"local_ip": "10.9.0.150",
|
|
"ip_range": "10.9.0.100-10.9.0.200",
|
|
}},
|
|
{"client_subnet too small", map[string]interface{}{
|
|
"ip_range": "10.9.0.100-10.9.0.200",
|
|
"client_subnet": "10.9.0.0/30",
|
|
}},
|
|
{"bad username whitespace", map[string]interface{}{
|
|
"username": "bad user",
|
|
}},
|
|
}
|
|
for _, tc := range cases {
|
|
if _, err := s.Validate(tc.cfg); err == nil {
|
|
t.Errorf("%s: expected error, got nil", tc.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildChapSecrets(t *testing.T) {
|
|
// With default user only
|
|
secrets := buildChapSecrets("vpnuser", "s3cret", nil)
|
|
if !strings.Contains(secrets, "vpnuser") || !strings.Contains(secrets, "s3cret") {
|
|
t.Errorf("chap-secrets missing default user:\n%s", secrets)
|
|
}
|
|
if !strings.Contains(secrets, "l2tpd") {
|
|
t.Errorf("chap-secrets missing server name:\n%s", secrets)
|
|
}
|
|
|
|
// With dial users
|
|
users := []service.DialUser{
|
|
{Username: "alice", Password: "pw1"},
|
|
{Username: "bob", Password: "pw2"},
|
|
}
|
|
secrets = buildChapSecrets("admin", "pass", users)
|
|
for _, u := range []string{"admin", "alice", "bob"} {
|
|
if !strings.Contains(secrets, u) {
|
|
t.Errorf("chap-secrets missing user %q:\n%s", u, secrets)
|
|
}
|
|
}
|
|
|
|
// Duplicate user should be deduplicated
|
|
secrets = buildChapSecrets("alice", "override", []service.DialUser{{Username: "alice", Password: "ignored"}})
|
|
count := strings.Count(secrets, "alice")
|
|
if count != 1 {
|
|
t.Errorf("expected alice once, got %d times in:\n%s", count, secrets)
|
|
}
|
|
|
|
// Empty default user — dial users still appear
|
|
secrets = buildChapSecrets("", "", []service.DialUser{{Username: "x", Password: "y"}})
|
|
if !strings.Contains(secrets, "x") {
|
|
t.Errorf("chap-secrets should still include dial users:\n%s", secrets)
|
|
}
|
|
if strings.Contains(secrets, `""`) {
|
|
t.Errorf("empty default user should not produce empty-quoted entry:\n%s", secrets)
|
|
}
|
|
}
|
|
|
|
func TestParseIPRange(t *testing.T) {
|
|
cases := []struct {
|
|
input string
|
|
wantErr bool
|
|
start string
|
|
end string
|
|
}{
|
|
{"10.0.0.1-10.0.0.100", false, "10.0.0.1", "10.0.0.100"},
|
|
{"10.0.0.1", true, "", ""},
|
|
{"10.0.0.1-10.0.0.2-10.0.0.3", true, "", ""},
|
|
{"not-ip-also-not", true, "", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
start, end, err := parseIPRange(tc.input)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Errorf("parseIPRange(%q): expected error", tc.input)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("parseIPRange(%q): unexpected error: %v", tc.input, err)
|
|
continue
|
|
}
|
|
if start.String() != tc.start || end.String() != tc.end {
|
|
t.Errorf("parseIPRange(%q): got %s-%s, want %s-%s", tc.input, start, end, tc.start, tc.end)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnsureExclusiveListener(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
t.Setenv("VPN_DATA_DIR", dataDir)
|
|
if err := store.Init(dataDir); err != nil {
|
|
t.Fatalf("store.Init: %v", err)
|
|
}
|
|
|
|
// Create a running L2TP tunnel in DB
|
|
running := &model.Tunnel{
|
|
Name: "l2tp-existing",
|
|
Protocol: "l2tp",
|
|
State: "running",
|
|
}
|
|
if err := store.DB.Create(running).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
s := &Service{}
|
|
newTunnel := &model.Tunnel{ID: running.ID + 1}
|
|
if err := s.ensureExclusiveListener(newTunnel); err == nil {
|
|
t.Fatal("expected error when another L2TP instance is running")
|
|
}
|
|
|
|
// After marking the existing one as stopped, it should pass
|
|
store.DB.Model(running).Update("state", "stopped")
|
|
if err := s.ensureExclusiveListener(newTunnel); err != nil {
|
|
t.Fatalf("expected no error after existing stopped: %v", err)
|
|
}
|
|
}
|