[GH-ISSUE #141] Zero-payload UDP packets during handshake #74

Open
opened 2026-10-13 21:03:20 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @orange-banal-pole on GitHub (May 25, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amneziawg-go/issues/141

Originally assigned to: @ygurov on GitHub.

coalesceMessages incorrectly emits empty UDP packets when empty buffers are present.

If the config contains sequential empty-valued keys such as:

I1 = <b 0x...>
I2 = <b 0x...>
I3 = <b 0x...>
I4 =
I5 =

(the AmneziaVPN client currently does not ignore empty-valued keys), coalesceMessages may generate an empty ipv6.Message, resulting in an empty UDP packet being sent.

This happens because trailing empty buffers are still coalesced into a separate message instead of being skipped entirely.

The following unit test reproduces the issue (add it to conn/bind_std_test.go):

func Test_coalesceEmptyMessages(t *testing.T) {
	cases := []struct {
		name     string
		buffs    [][]byte
		wantLens []int
		wantGSO  []int
	}{
		{
			name: "one non-empty buffer with two empty buffers",
			buffs: [][]byte{
				make([]byte, 1, 1),
				[]byte{},
				[]byte{},
			},
			wantLens: []int{1},
			wantGSO:  []int{0},
		},
	}

	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			addr := &net.UDPAddr{
				IP:   net.ParseIP("127.0.0.1").To4(),
				Port: 1,
			}

			msgs := make([]ipv6.Message, len(tt.buffs))
			for i := range msgs {
				msgs[i].Buffers = make([][]byte, 1)
				msgs[i].OOB = make([]byte, 0, 2)
			}

			got := coalesceMessages(
				addr,
				&StdNetEndpoint{AddrPort: addr.AddrPort()},
				tt.buffs,
				msgs,
				mockSetGSOSize,
			)

			if got != len(tt.wantLens) {
				t.Fatalf("got len %d, want %d", got, len(tt.wantLens))
			}

			for i := range got {
				if msgs[i].Addr != addr {
					t.Errorf("msgs[%d].Addr != passed addr", i)
				}

				gotLen := len(msgs[i].Buffers[0])
				if gotLen != tt.wantLens[i] {
					t.Errorf(
						"len(msgs[%d].Buffers[0]) = %d, want %d",
						i,
						gotLen,
						tt.wantLens[i],
					)
				}

				gotGSO, err := mockGetGSOSize(msgs[i].OOB)
				if err != nil {
					t.Fatalf("msgs[%d] getGSOSize err: %v", i, err)
				}

				if gotGSO != tt.wantGSO[i] {
					t.Errorf(
						"msgs[%d] gsoSize = %d, want %d",
						i,
						gotGSO,
						tt.wantGSO[i],
					)
				}
			}
		})
	}
}

Current behavior:

$ go test -run=coalesceEmpty
--- FAIL: Test_coalesceEmptyMessages (0.00s)
    --- FAIL: Test_coalesceEmptyMessages/one_non-empty_buffer_with_two_empty_buffers (0.00s)
        bind_std_test.go:284: got len 2 want: 1

Expected behavior:

Empty buffers should be ignored completely.
coalesceMessages should never emit an empty ipv6.Message.
No zero-length UDP packets should be transmitted.

Originally created by @orange-banal-pole on GitHub (May 25, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amneziawg-go/issues/141 Originally assigned to: @ygurov on GitHub. `coalesceMessages` incorrectly emits empty UDP packets when empty buffers are present. If the config contains sequential empty-valued keys such as: ``` I1 = <b 0x...> I2 = <b 0x...> I3 = <b 0x...> I4 = I5 = ``` (the AmneziaVPN client currently does not ignore empty-valued keys), `coalesceMessages` may generate an empty `ipv6.Message`, resulting in an empty UDP packet being sent. This happens because trailing empty buffers are still coalesced into a separate message instead of being skipped entirely. The following unit test reproduces the issue (add it to `conn/bind_std_test.go`): ``` go func Test_coalesceEmptyMessages(t *testing.T) { cases := []struct { name string buffs [][]byte wantLens []int wantGSO []int }{ { name: "one non-empty buffer with two empty buffers", buffs: [][]byte{ make([]byte, 1, 1), []byte{}, []byte{}, }, wantLens: []int{1}, wantGSO: []int{0}, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { addr := &net.UDPAddr{ IP: net.ParseIP("127.0.0.1").To4(), Port: 1, } msgs := make([]ipv6.Message, len(tt.buffs)) for i := range msgs { msgs[i].Buffers = make([][]byte, 1) msgs[i].OOB = make([]byte, 0, 2) } got := coalesceMessages( addr, &StdNetEndpoint{AddrPort: addr.AddrPort()}, tt.buffs, msgs, mockSetGSOSize, ) if got != len(tt.wantLens) { t.Fatalf("got len %d, want %d", got, len(tt.wantLens)) } for i := range got { if msgs[i].Addr != addr { t.Errorf("msgs[%d].Addr != passed addr", i) } gotLen := len(msgs[i].Buffers[0]) if gotLen != tt.wantLens[i] { t.Errorf( "len(msgs[%d].Buffers[0]) = %d, want %d", i, gotLen, tt.wantLens[i], ) } gotGSO, err := mockGetGSOSize(msgs[i].OOB) if err != nil { t.Fatalf("msgs[%d] getGSOSize err: %v", i, err) } if gotGSO != tt.wantGSO[i] { t.Errorf( "msgs[%d] gsoSize = %d, want %d", i, gotGSO, tt.wantGSO[i], ) } } }) } } ``` Current behavior: ``` $ go test -run=coalesceEmpty --- FAIL: Test_coalesceEmptyMessages (0.00s) --- FAIL: Test_coalesceEmptyMessages/one_non-empty_buffer_with_two_empty_buffers (0.00s) bind_std_test.go:284: got len 2 want: 1 ``` Expected behavior: Empty buffers should be ignored completely. `coalesceMessages` should never emit an empty `ipv6.Message`. No zero-length UDP packets should be transmitted.
Author
Owner

@ygurov commented on GitHub (Jun 17, 2026):

Hi @orange-banal-pole,
Thanks for reporting this one. I have introduced some changes to prevent this behavior on the junk-generation side: this would simply ignore any empty value treating it as not actually set one github.com/amnezia-vpn/amneziawg-go@1cc94272ca

<!-- gh-comment-id:4736164653 --> @ygurov commented on GitHub (Jun 17, 2026): Hi @orange-banal-pole, Thanks for reporting this one. I have introduced some changes to prevent this behavior on the junk-generation side: this would simply ignore any empty value treating it as not actually set one https://github.com/amnezia-vpn/amneziawg-go/commit/1cc94272ca8e9e223a5fe76382f5880f09d3c12d
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amnezia-vpn/amneziawg-go#74
No description provided.