feat(amneziawg): support persistent keepalive ranges (#6881)

AmneziaWG clients accept a randomised PersistentKeepalive range such as 15-25,
which makes keepalive timing harder to fingerprint; the panel only stored a
single number. A client's keepalive may now be a range, validated the way
AmneziaWG parses it. The downloadable .conf and vpn:// profile carry the range,
Clash/Mihomo output gets its lower bound, and plain WireGuard stays scalar-only.
The range lives in an additive wg_keep_alive_range column, so the integer
column and an older panel binary keep working after a rollback.
This commit is contained in:
YoungReckless4
2026-10-24 07:36:49 +03:00
committed by GitHub
parent 207c705300
commit 35e09191de
59 changed files with 1871 additions and 146 deletions
+7
View File
@@ -86,6 +86,10 @@ func (g *exampleGen) forType(t TypeRef, visited map[string]bool) any {
return []any{g.forType(*t.Element, visited)}
case KindMap:
return map[string]any{}
case KindUnion:
if len(t.Variants) > 0 {
return g.forType(t.Variants[0], visited)
}
case KindRef:
if t.Name == "nullable" {
return nil
@@ -105,6 +109,9 @@ func (g *exampleGen) forType(t TypeRef, visited map[string]bool) any {
}
func baseKind(t TypeRef) TypeRef {
if t.Kind == KindUnion && len(t.Variants) > 0 {
return t.Variants[0]
}
if t.Kind == KindRef && t.Name == "nullable" && t.Inner != nil {
return *t.Inner
}
+6
View File
@@ -110,6 +110,12 @@ func (g *schemaGen) typeSchema(t TypeRef) map[string]any {
return map[string]any{"type": "array", "items": g.typeSchema(*t.Element)}
case KindMap:
return map[string]any{"type": "object", "additionalProperties": g.typeSchema(*t.Value)}
case KindUnion:
variants := make([]any, 0, len(t.Variants))
for _, variant := range t.Variants {
variants = append(variants, g.typeSchema(variant))
}
return map[string]any{"oneOf": variants}
case KindAny, KindUnknown, KindRaw:
return map[string]any{}
case KindRef:
+7
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"sort"
"strings"
)
func emitTypes(w io.Writer, schemas []Schema, aliases []Alias) error {
@@ -60,6 +61,12 @@ func tsTypeExpr(t TypeRef) string {
return tsTypeExpr(*t.Element) + "[]"
case KindMap:
return "Record<" + tsTypeExpr(*t.Key) + ", " + tsTypeExpr(*t.Value) + ">"
case KindUnion:
parts := make([]string, 0, len(t.Variants))
for _, variant := range t.Variants {
parts = append(parts, tsTypeExpr(variant))
}
return strings.Join(parts, " | ")
case KindRef:
if t.Name == "nullable" {
return tsTypeExpr(*t.Inner) + " | null"
+6
View File
@@ -70,6 +70,12 @@ func zodTypeExpr(t TypeRef) string {
return "z.array(" + zodTypeExpr(*t.Element) + ")"
case KindMap:
return "z.record(" + zodTypeExpr(*t.Key) + ", " + zodTypeExpr(*t.Value) + ")"
case KindUnion:
parts := make([]string, 0, len(t.Variants))
for _, variant := range t.Variants {
parts = append(parts, zodTypeExpr(variant))
}
return "z.union([" + strings.Join(parts, ", ") + "])"
case KindRef:
if t.Name == "nullable" {
return zodTypeExpr(*t.Inner) + ".nullable()"
+4
View File
@@ -48,7 +48,11 @@ func run(root, outDir string) error {
{Field: "StreamSettings", Kind: KindAny},
{Field: "Sniffing", Kind: KindAny},
},
"Client": {
{Field: "KeepAlive", Type: TypeRef{Kind: KindUnion, Variants: []TypeRef{{Kind: KindInt}, {Kind: KindString}}}},
},
"ClientRecord": {
{Field: "KeepAlive", Type: TypeRef{Kind: KindUnion, Variants: []TypeRef{{Kind: KindInt}, {Kind: KindString}}}},
{Field: "Reverse", Kind: KindAny},
},
"InboundClientIps": {
+8 -6
View File
@@ -31,12 +31,13 @@ type Field struct {
}
type TypeRef struct {
Kind TypeKind
Name string
Element *TypeRef
Key *TypeRef
Value *TypeRef
Inner *TypeRef
Kind TypeKind
Name string
Element *TypeRef
Key *TypeRef
Value *TypeRef
Inner *TypeRef
Variants []TypeRef
}
type TypeKind string
@@ -48,6 +49,7 @@ const (
KindBool TypeKind = "boolean"
KindArray TypeKind = "array"
KindMap TypeKind = "map"
KindUnion TypeKind = "union"
KindObject TypeKind = "object"
KindRef TypeKind = "ref"
KindUnknown TypeKind = "unknown"
+11 -2
View File
@@ -13,6 +13,7 @@ import (
type walkOverride struct {
Field string
Kind TypeKind
Type TypeRef
}
type packageRequest struct {
@@ -122,7 +123,11 @@ func buildFields(fld *ast.Field, overrides []walkOverride) []Field {
t := exprToType(fld.Type)
for _, o := range overrides {
if o.Field == n.Name || o.Field == jsonName {
t = TypeRef{Kind: o.Kind}
if o.Type.Kind != "" {
t = o.Type
} else {
t = TypeRef{Kind: o.Kind}
}
break
}
}
@@ -145,7 +150,11 @@ func buildFields(fld *ast.Field, overrides []walkOverride) []Field {
t := exprToType(fld.Type)
for _, o := range overrides {
if o.Field == exprIdentName(fld.Type) || o.Field == jsonName {
t = TypeRef{Kind: o.Kind}
if o.Type.Kind != "" {
t = o.Type
} else {
t = TypeRef{Kind: o.Kind}
}
break
}
}
+25
View File
@@ -43,3 +43,28 @@ type HappLinkResult struct {
t.Fatalf("generated types expose DI interface:\n%s", apiSurface)
}
}
func TestKeepAliveOverrideEmitsNumberOrString(t *testing.T) {
dir := t.TempDir()
source := "package model\ntype Client struct { KeepAlive *int `json:\"keepAlive,omitempty\"` }\n"
if err := os.WriteFile(filepath.Join(dir, "client.go"), []byte(source), 0o644); err != nil {
t.Fatalf("write fixture: %v", err)
}
union := TypeRef{Kind: KindUnion, Variants: []TypeRef{{Kind: KindInt}, {Kind: KindString}}}
schemas, _, err := walkPackages([]packageRequest{{Path: dir, StructAllow: setOf("Client"), Overrides: map[string][]walkOverride{"Client": {{Field: "KeepAlive", Type: union}}}}})
if err != nil {
t.Fatalf("walk fixture: %v", err)
}
var types bytes.Buffer
if err := emitTypes(&types, schemas, nil); err != nil || !strings.Contains(types.String(), "keepAlive?: number | string;") {
t.Fatalf("generated TypeScript keepAlive = %s, err = %v", types.String(), err)
}
var zod bytes.Buffer
if err := emitZod(&zod, schemas, nil); err != nil || !strings.Contains(zod.String(), "z.union([z.number().int(), z.string()]).optional()") {
t.Fatalf("generated Zod keepAlive = %s, err = %v", zod.String(), err)
}
var schema bytes.Buffer
if err := emitJSONSchema(&schema, schemas, nil); err != nil || !strings.Contains(schema.String(), `"oneOf"`) {
t.Fatalf("generated JSON schema keepAlive = %s, err = %v", schema.String(), err)
}
}