feat: 日志中心 + GRE协议支持 + 流量监控 + 部署脚本
- 新增日志中心功能 (api/logapi.go + Logs.vue) - 新增GRE协议服务支持 (service/gre/) - 新增流量监控组件 (TrafficChart.vue) - 新增L2TP/IPSec测试用例 - 新增日志轮转服务 (logrotate.go) - 新增部署脚本 (scripts/install.sh, uninstall.sh, service文件) - 更新i18n国际化文件 - 优化后端API和前端路由
This commit is contained in:
1 parent
7de1f70905
commit
a14d9e62e6
25 files changed
+2285
-25
No files matched your search
@@ -0,0 +1,180 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user