mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 07:15:01 +03:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user