Compare commits

...
11 Commits
Author SHA1 Message Date
Yaroslav Gurov b5928efb6c fix: disable the whole underload if DisableCookies is on
Thanks to @darkk for spotting and suggesting the fix
2026-08-28 17:48:27 +02:00
Yaroslav Gurov da11c9fadc fix: use udp window for RandomPaddingAddition 2026-08-28 17:48:05 +02:00
Yaroslav Gurov 1b86b2ae0e fix: wrong trailerLen for HandshakeCookie message
* AWG3.1-related changes indroduced a new bug with HandshakeCookie
  buffer size. Instead of adding trailerLen to the final size, it is
getting provided as a capacity param leading to the wrong behavior and
runtime panic while RandomTrailers is used
* Must be the typo :_)
2026-08-13 21:36:22 +02:00
Yaroslav Gurov 08271d00b3 feat: make PeekLookAtSocketFd available across all platforms 2026-08-13 15:55:40 +02:00
Yaroslav Gurov 75ea550a64 chore: update awg-tools 2026-08-13 13:39:15 +02:00
Yaroslav Gurov 1f50ad736e feat: awg 3.1 features
* add RandomTrailers feature which appends random amount of bytes to the
end of each packet
* add DisableCookie feature which prohibits interface to send any cookie
replies
2026-08-13 00:25:03 +02:00
Yaroslav Gurov 08d68cdae2 fix: keepalives are ignored 2026-08-05 13:59:10 +02:00
itfsdev cf9d2dd202 fix: pin Dockerfile's amneziawg-tools build to the tagged v3.0.20260730 release 2026-07-31 20:16:15 +08:00
Yaroslav Gurov 7860d60a52 fix: change HeaderCipherNonceSize to be reported in wrong S1-S4 2026-07-31 08:53:19 +02:00
Yaroslav Gurov ce7cf1034d docs: change 8 requirement to 12 in README 2026-07-31 08:53:19 +02:00
itfsdev d57d98d55b feat: update Dockerfile to point to the special commit from feat/awg3 from amneziawg-tools 2026-07-28 22:40:25 +08:00
10 changed files with 197 additions and 74 deletions

No files matched your search

+13 -7
View File
@@ -5,14 +5,20 @@ RUN go mod download && \
go mod verify && \
go build -ldflags '-linkmode external -extldflags "-fno-PIC -static"' -v -o /usr/bin
FROM alpine:3.19
ARG AWGTOOLS_RELEASE="1.0.20250901"
FROM alpine:3.19 as tools
ARG AWGTOOLS_COMMIT="v3.1.20260812"
RUN apk --no-cache add iproute2 iptables bash && \
cd /usr/bin/ && \
wget https://github.com/amnezia-vpn/amneziawg-tools/releases/download/v${AWGTOOLS_RELEASE}/alpine-3.19-amneziawg-tools.zip && \
unzip -j alpine-3.19-amneziawg-tools.zip && \
chmod +x /usr/bin/awg /usr/bin/awg-quick && \
RUN apk add --no-cache git build-base linux-headers && \
git clone https://github.com/amnezia-vpn/amneziawg-tools.git /amneziawg-tools && \
cd /amneziawg-tools && git checkout ${AWGTOOLS_COMMIT} && \
cd src && make
FROM alpine:3.19
RUN apk --no-cache add iproute2 iptables bash
COPY --from=tools /amneziawg-tools/src/wg /usr/bin/awg
COPY --from=tools /amneziawg-tools/src/wg-quick/linux.bash /usr/bin/awg-quick
RUN chmod +x /usr/bin/awg /usr/bin/awg-quick && \
ln -s /usr/bin/awg /usr/bin/wg && \
ln -s /usr/bin/awg-quick /usr/bin/wg-quick
COPY --from=awg /usr/bin/amneziawg-go /usr/bin/amneziawg-go
+1 -1
View File
@@ -78,7 +78,7 @@ Header protection is the mechanism of protecting low-entropy values of packets'
> Use `awg genkey` to generate header protection key
> [!IMPORTANT]
> Header protection requires `S1-S4` value to be 8 at least
> Header protection requires `S1-S4` value to be 12 at least
### Content padding [AWG 3+]
+18
View File
@@ -582,6 +582,24 @@ func (bind *WinRingBind) BindSocketToInterface6(interfaceIndex uint32, blackhole
return nil
}
func (bind *WinRingBind) PeekLookAtSocketFd4() (fd int, err error) {
bind.mu.RLock()
defer bind.mu.RUnlock()
if bind.isOpen.Load() != 1 {
return -1, net.ErrClosed
}
return int(bind.v4.sock), nil
}
func (bind *WinRingBind) PeekLookAtSocketFd6() (fd int, err error) {
bind.mu.RLock()
defer bind.mu.RUnlock()
if bind.isOpen.Load() != 1 {
return -1, net.ErrClosed
}
return int(bind.v6.sock), nil
}
func bindSocketToInterface4(handle windows.Handle, interfaceIndex uint32) error {
const IP_UNICAST_IF = 31
/* MSDN says for IPv4 this needs to be in net byte order, so that it's like an IP address with leading zeros. */
File renamed without changes.
+1
View File
@@ -24,6 +24,7 @@ const (
CookieRefreshTime = time.Second * 120
HandshakeInitationRate = time.Second / 50
PaddingMultiple = 16
DefaultUdpWindow = 500
)
const (
+3
View File
@@ -126,6 +126,9 @@ type Device struct {
keepaliveTimeoutSec AtomicUintRange
maxHandshakeAttemps AtomicUintRange
}
randomTrailers atomic.Bool
disableCookies atomic.Bool
}
// deviceState represents the state of a Device.
+6
View File
@@ -57,6 +57,7 @@ type Peer struct {
cookieGenerator CookieGenerator
trieEntries list.List
persistentKeepaliveInterval AtomicUintRange
udpWindow atomic.Uint32
}
func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) {
@@ -79,6 +80,8 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) {
// create peer
peer := new(Peer)
peer.udpWindow.Store(DefaultUdpWindow)
peer.cookieGenerator.Init(pk)
peer.device = device
peer.queue.outbound = newAutodrainingOutboundQueue(device)
@@ -283,6 +286,9 @@ func (peer *Peer) SetEndpointFromPacket(endpoint conn.Endpoint) {
if peer.endpoint.disableRoaming {
return
}
if peer.endpoint.val != endpoint {
peer.udpWindow.Store(DefaultUdpWindow)
}
peer.endpoint.clearSrcOnTx = false
peer.endpoint.val = endpoint
}
+49 -38
View File
@@ -154,8 +154,12 @@ func (device *Device) RoutineReceiveIncoming(
}
// get message padding and type based on information from S1-S4 and H1-H4
msgType, padding := device.DeterminePacketTypeAndPadding(packet, MessageUnknownType, typeHash)
msgSize, msgType, padding := device.DeterminePacketTypeAndPadding(packet, typeHash)
packet = packet[padding:]
if msgType != MessageTransportType {
packet = packet[:msgSize]
}
if cip != nil {
applyHash(packet[:4], packet[:4], typeHash)
@@ -365,7 +369,8 @@ func (device *Device) RoutineHandshake(id int) {
// endpoints destination address is the source of the datagram
if device.IsUnderLoad() {
disableCookies := device.disableCookies.Load()
if !disableCookies && device.IsUnderLoad() {
// verify MAC2 field
@@ -510,7 +515,12 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) {
}
rxBytesLen += uint64(len(elem.packet) + MinMessageSize)
if len(elem.packet) == 0 {
udpWindow := elem.padding + MessageTransportHeaderSize + uint32(len(elem.packet))
if peer.udpWindow.Load() < udpWindow {
peer.udpWindow.Store(udpWindow)
}
if len(elem.packet) == 0 || elem.packet[0] == 0 {
device.log.Verbosef("%v - Receiving keepalive packet", peer)
continue
}
@@ -558,7 +568,7 @@ func (peer *Peer) RoutineSequentialReceiver(maxBatchSize int) {
continue
}
bufs = append(bufs, elem.buffer[int(elem.padding):int(elem.padding)+MessageTransportOffsetContent+len(elem.packet)])
bufs = append(bufs, elem.buffer[int(elem.padding):int(elem.padding)+MessageTransportHeaderSize+len(elem.packet)])
}
peer.rxBytes.Add(rxBytesLen)
@@ -592,57 +602,58 @@ func applyHash(dst, src, hash []byte) {
}
}
func (device *Device) DeterminePacketTypeAndPadding(packet []byte, expectedType uint32, typeHash []byte) (uint32, uint32) {
func (device *Device) DeterminePacketTypeAndPadding(packet []byte, typeHash []byte) (int, uint32, uint32) {
var headerBytes [4]byte
var padding uint32
var header UintRange
var expectedSize int
size := len(packet)
randomTrailers := device.randomTrailers.Load()
if expectedType == MessageUnknownType || expectedType == MessageInitiationType {
padding := device.paddings.init.Load()
header := device.headers.init.Load()
padding = device.paddings.init.Load()
header = device.headers.init.Load()
expectedSize = int(padding) + MessageInitiationSize
if size == int(padding)+MessageInitiationSize {
applyHash(headerBytes[:], packet[padding:padding+4], typeHash)
if header.Contains(binary.LittleEndian.Uint32(headerBytes[:])) {
return MessageInitiationType, padding
}
if size == expectedSize || randomTrailers && size > expectedSize {
applyHash(headerBytes[:], packet[padding:padding+4], typeHash)
if header.Contains(binary.LittleEndian.Uint32(headerBytes[:])) {
return MessageInitiationSize, MessageInitiationType, padding
}
}
if expectedType == MessageUnknownType || expectedType == MessageResponseType {
padding := device.paddings.response.Load()
header := device.headers.response.Load()
padding = device.paddings.response.Load()
header = device.headers.response.Load()
expectedSize = int(padding) + MessageResponseSize
if size == int(padding)+MessageResponseSize {
applyHash(headerBytes[:], packet[padding:padding+4], typeHash)
if header.Contains(binary.LittleEndian.Uint32(headerBytes[:])) {
return MessageResponseType, padding
}
if size == expectedSize || randomTrailers && size > expectedSize {
applyHash(headerBytes[:], packet[padding:padding+4], typeHash)
if header.Contains(binary.LittleEndian.Uint32(headerBytes[:])) {
return MessageResponseSize, MessageResponseType, padding
}
}
if expectedType == MessageUnknownType || expectedType == MessageCookieReplyType {
padding := device.paddings.cookie.Load()
header := device.headers.cookie.Load()
padding = device.paddings.cookie.Load()
header = device.headers.cookie.Load()
expectedSize = int(padding) + MessageCookieReplySize
if size == int(padding)+MessageCookieReplySize {
applyHash(headerBytes[:], packet[padding:padding+4], typeHash)
if header.Contains(binary.LittleEndian.Uint32(headerBytes[:])) {
return MessageCookieReplyType, padding
}
if size == expectedSize || randomTrailers && size > expectedSize {
applyHash(headerBytes[:], packet[padding:padding+4], typeHash)
if header.Contains(binary.LittleEndian.Uint32(headerBytes[:])) {
return MessageCookieReplySize, MessageCookieReplyType, padding
}
}
if expectedType == MessageUnknownType || expectedType == MessageTransportType {
padding := device.paddings.transport.Load()
header := device.headers.transport.Load()
padding = device.paddings.transport.Load()
header = device.headers.transport.Load()
expectedSize = int(padding) + MessageTransportSize
if size >= int(padding)+MessageTransportHeaderSize {
applyHash(headerBytes[:], packet[padding:padding+4], typeHash)
if header.Contains(binary.LittleEndian.Uint32(headerBytes[:])) {
return MessageTransportType, padding
}
if size >= expectedSize {
applyHash(headerBytes[:], packet[padding:padding+4], typeHash)
if header.Contains(binary.LittleEndian.Uint32(headerBytes[:])) {
return MessageTransportSize, MessageTransportType, padding
}
}
return MessageUnknownType, 0
return 0, MessageUnknownType, 0
}
+75 -27
View File
@@ -48,12 +48,13 @@ import (
*/
type QueueOutboundElement struct {
buffer *[MaxMessageSize]byte // slice holding the packet data
packet []byte // slice of "buffer" (always!)
nonce uint64 // nonce for encryption
keypair *Keypair // keypair for encryption
peer *Peer // related peer
padding uint32
buffer *[MaxMessageSize]byte // slice holding the packet data
packet []byte // slice of "buffer" (always!)
nonce uint64 // nonce for encryption
keypair *Keypair // keypair for encryption
peer *Peer // related peer
padding uint32
isKeepalive bool
}
type QueueOutboundElementsContainer struct {
@@ -66,6 +67,7 @@ func (device *Device) NewOutboundElement() *QueueOutboundElement {
elem.buffer = device.GetMessageBuffer()
elem.nonce = 0
elem.padding = device.paddings.transport.Load()
elem.isKeepalive = false
// keypair and peer were cleared (if necessary) by clearPointers.
return elem
}
@@ -86,6 +88,7 @@ func (elem *QueueOutboundElement) clearPointers() {
func (peer *Peer) SendKeepalive() {
if len(peer.queue.staged) == 0 && peer.isRunning.Load() {
elem := peer.device.NewOutboundElement()
elem.isKeepalive = true
elemsContainer := peer.device.GetOutboundElementsContainer()
elemsContainer.elems = append(elemsContainer.elems, elem)
select {
@@ -143,8 +146,10 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
sendBuffer = append(sendBuffer, peer.device.JunkPackets()...)
padding := peer.device.paddings.init.Load()
buf := make([]byte, padding+MessageInitiationSize)
padding := int(peer.device.paddings.init.Load())
trailerLen := max(peer.randomTrailer(padding+MessageInitiationSize), 0)
buf := make([]byte, padding+MessageInitiationSize+trailerLen)
crypt := buf[:padding]
rand.Read(crypt)
@@ -165,6 +170,9 @@ func (peer *Peer) SendHandshakeInitiation(isRetry bool) error {
cip.XORKeyStream(packet, packet)
}
trailer := buf[padding+MessageInitiationSize:]
rand.Read(trailer)
sendBuffer = append(sendBuffer, buf)
err = peer.SendBuffers(sendBuffer)
if err != nil {
@@ -188,8 +196,10 @@ func (peer *Peer) SendHandshakeResponse() error {
return err
}
padding := peer.device.paddings.response.Load()
buf := make([]byte, padding+MessageResponseSize)
padding := int(peer.device.paddings.response.Load())
trailerLen := max(peer.randomTrailer(padding+MessageResponseSize), 0)
buf := make([]byte, padding+MessageResponseSize+trailerLen)
crypt := buf[:padding]
rand.Read(crypt)
@@ -217,6 +227,9 @@ func (peer *Peer) SendHandshakeResponse() error {
cip.XORKeyStream(packet, packet)
}
trailer := buf[padding+MessageResponseSize:]
rand.Read(trailer)
// TODO: allocation could be avoided
err = peer.SendBuffers([][]byte{buf})
if err != nil {
@@ -242,8 +255,10 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement)
return err
}
padding := device.paddings.cookie.Load()
buf := make([]byte, padding+MessageCookieReplySize)
padding := int(device.paddings.cookie.Load())
trailerLen := max(device.randomTrailer(padding+MessageCookieReplySize), 0)
buf := make([]byte, padding+MessageCookieReplySize+trailerLen)
crypt := buf[:padding]
rand.Read(crypt)
@@ -260,6 +275,9 @@ func (device *Device) SendHandshakeCookie(initiatingElem *QueueHandshakeElement)
cip.XORKeyStream(packet, packet)
}
trailer := buf[padding+MessageCookieReplySize:]
rand.Read(trailer)
// TODO: allocation could be avoided
device.net.bind.Send([][]byte{buf}, initiatingElem.endpoint)
return nil
@@ -507,27 +525,49 @@ func calculatePaddingSize(packetSize, mtu int) int {
return paddedSize - lastUnit
}
func (device *Device) randomPaddingAddition(packetSize, mtu int) int {
addition := device.contentPaddingAddition.Load()
func (peer *Peer) randomPaddingAddition(packetSize int) int {
addition := peer.device.contentPaddingAddition.Load()
if addition.IsZero() {
return -1
}
add := int(addition.PickOne())
if mtu != 0 {
if packetSize > mtu {
packetSize %= mtu
}
udpWindow := int(peer.udpWindow.Load())
if udpWindow < packetSize {
return 0
}
space := mtu - packetSize
if add > space {
add = space
}
add := int(addition.PickOne())
space := udpWindow - packetSize
if add > space {
add = space
}
return add
}
func (device *Device) randomTrailer(packetSize int) int {
if !device.randomTrailers.Load() {
return -1
}
if DefaultUdpWindow < packetSize {
return 0
}
return int(fastrandn(uint32(DefaultUdpWindow - packetSize)))
}
func (peer *Peer) randomTrailer(packetSize int) int {
if !peer.device.randomTrailers.Load() {
return -1
}
udpWindow := int(peer.udpWindow.Load())
if udpWindow < packetSize {
return 0
}
return int(fastrandn(uint32(udpWindow - packetSize)))
}
/* Encrypts the elements in the queue
* and marks them for sequential consumption (by releasing the mutex)
*
@@ -541,6 +581,11 @@ func (device *Device) RoutineEncryption(id int) {
for elemsContainer := range device.queue.encryption.c {
for _, elem := range elemsContainer.elems {
udpWindow := elem.padding + MinMessageSize + uint32(len(elem.packet))
if elem.peer.udpWindow.Load() < udpWindow {
elem.peer.udpWindow.Store(udpWindow)
}
// fill crypto padding
crypt := elem.buffer[:elem.padding]
rand.Read(crypt)
@@ -556,13 +601,16 @@ func (device *Device) RoutineEncryption(id int) {
binary.LittleEndian.PutUint32(fieldReceiver, elem.keypair.remoteIndex)
binary.LittleEndian.PutUint64(fieldNonce, elem.nonce)
packetSize := len(elem.packet)
packetSize := len(elem.packet) + MinMessageSize + int(elem.padding)
mtu := int(device.tun.mtu.Load())
paddingSize := device.randomPaddingAddition(packetSize, mtu)
paddingSize := elem.peer.randomPaddingAddition(packetSize)
if paddingSize < 0 {
paddingSize = elem.peer.randomTrailer(packetSize)
}
if paddingSize < 0 {
// pad content to multiple of 16
paddingSize = calculatePaddingSize(packetSize, mtu)
paddingSize = calculatePaddingSize(len(elem.packet), mtu)
}
// append trailing zeroes
@@ -628,7 +676,7 @@ func (peer *Peer) RoutineSequentialSender(maxBatchSize int) {
dataSent := false
elemsContainer.Lock()
for _, elem := range elemsContainer.elems {
if len(elem.packet) != MessageKeepaliveSize {
if !elem.isKeepalive {
dataSent = true
}
+31 -1
View File
@@ -70,6 +70,18 @@ func (device *Device) IpcGetOperation(w io.Writer) error {
}
buf.WriteByte('\n')
}
boolf := func(prefix string, val bool) {
buf.Grow(3 + len(prefix))
buf.WriteString(prefix)
buf.WriteByte('=')
if val {
buf.WriteByte('1')
} else {
buf.WriteByte('0')
}
buf.WriteByte('\n')
}
func() {
// lock required resources
@@ -173,6 +185,8 @@ func (device *Device) IpcGetOperation(w io.Writer) error {
if rang := device.timings.maxHandshakeAttemps.Load(); !rang.IsZero() {
sendf("max_handshake_attempts=%s", rang.ToString())
}
boolf("random_trailers", device.randomTrailers.Load())
boolf("disable_cookies", device.disableCookies.Load())
for _, peer := range device.peers.keyMap {
// Serialize peer state.
@@ -513,6 +527,22 @@ func (device *Device) handleDeviceLine(ipcDev *ipcSetDevice, key, value string)
device.log.Verbosef("UAPI: Updating max handshake attempts")
device.timings.maxHandshakeAttemps.Store(rang)
case "random_trailers":
val, err := strconv.ParseBool(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse random trailers: %w", err)
}
device.log.Verbosef("UAPI: Updating random trailers")
device.randomTrailers.Store(val)
case "disable_cookies":
val, err := strconv.ParseBool(value)
if err != nil {
return ipcErrorf(ipc.IpcErrorInvalid, "failed to parse disable cookies: %w", err)
}
device.log.Verbosef("UAPI: Updating disable cookies")
device.disableCookies.Store(val)
default:
return ipcErrorf(ipc.IpcErrorInvalid, "invalid UAPI device key: %v", key)
}
@@ -823,7 +853,7 @@ func (d *ipcSetDevice) mergeWithDevice(device *Device) error {
paddings := []uint32{d.paddings.init, d.paddings.response, d.paddings.cookie, d.paddings.transport}
for i, padding := range paddings {
if padding < HeaderCipherNonceSize {
return fmt.Errorf("S%d must be more then 8 to use headerProtection", i)
return fmt.Errorf("S%d must be more then %d to use headerProtection", i, HeaderCipherNonceSize)
}
}
}