View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.haproxy;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol.AddressFamily;
20  import io.netty.util.AbstractReferenceCounted;
21  import io.netty.util.CharsetUtil;
22  import io.netty.util.NetUtil;
23  import io.netty.util.ResourceLeakDetector;
24  import io.netty.util.ResourceLeakDetectorFactory;
25  import io.netty.util.ResourceLeakTracker;
26  import io.netty.util.internal.ObjectUtil;
27  import io.netty.util.internal.StringUtil;
28  
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.List;
32  
33  /**
34   * Message container for decoded HAProxy proxy protocol parameters
35   */
36  public final class HAProxyMessage extends AbstractReferenceCounted {
37  
38      // Let's pick some conservative limit here.
39      private static final int MAX_NESTING_LEVEL = 128;
40      private static final ResourceLeakDetector<HAProxyMessage> leakDetector =
41              ResourceLeakDetectorFactory.instance().newResourceLeakDetector(HAProxyMessage.class);
42  
43      private final ResourceLeakTracker<HAProxyMessage> leak;
44      private final HAProxyProtocolVersion protocolVersion;
45      private final HAProxyCommand command;
46      private final HAProxyProxiedProtocol proxiedProtocol;
47      private final String sourceAddress;
48      private final String destinationAddress;
49      private final int sourcePort;
50      private final int destinationPort;
51      private final List<HAProxyTLV> tlvs;
52  
53      /**
54       * Creates a new instance
55       */
56      private HAProxyMessage(
57              HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol,
58              String sourceAddress, String destinationAddress, String sourcePort, String destinationPort) {
59          this(
60                  protocolVersion, command, proxiedProtocol,
61                  sourceAddress, destinationAddress, portStringToInt(sourcePort), portStringToInt(destinationPort));
62      }
63  
64      /**
65       * Creates a new instance of HAProxyMessage.
66       * @param protocolVersion the protocol version.
67       * @param command the command.
68       * @param proxiedProtocol the protocol containing the address family and transport protocol.
69       * @param sourceAddress the source address.
70       * @param destinationAddress the destination address.
71       * @param sourcePort the source port. This value must be 0 for unix, unspec addresses.
72       * @param destinationPort the destination port. This value must be 0 for unix, unspec addresses.
73       */
74      public HAProxyMessage(
75              HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol,
76              String sourceAddress, String destinationAddress, int sourcePort, int destinationPort) {
77  
78          this(protocolVersion, command, proxiedProtocol,
79               sourceAddress, destinationAddress, sourcePort, destinationPort, Collections.<HAProxyTLV>emptyList());
80      }
81  
82      /**
83       * Creates a new instance of HAProxyMessage.
84       * @param protocolVersion the protocol version.
85       * @param command the command.
86       * @param proxiedProtocol the protocol containing the address family and transport protocol.
87       * @param sourceAddress the source address.
88       * @param destinationAddress the destination address.
89       * @param sourcePort the source port. This value must be 0 for unix, unspec addresses.
90       * @param destinationPort the destination port. This value must be 0 for unix, unspec addresses.
91       * @param tlvs the list of tlvs.
92       */
93      public HAProxyMessage(
94              HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol,
95              String sourceAddress, String destinationAddress, int sourcePort, int destinationPort,
96              List<? extends HAProxyTLV> tlvs) {
97  
98          ObjectUtil.checkNotNull(protocolVersion, "protocolVersion");
99          ObjectUtil.checkNotNull(proxiedProtocol, "proxiedProtocol");
100         ObjectUtil.checkNotNull(tlvs, "tlvs");
101         AddressFamily addrFamily = proxiedProtocol.addressFamily();
102 
103         checkAddress(sourceAddress, addrFamily);
104         checkAddress(destinationAddress, addrFamily);
105         checkPort(sourcePort, addrFamily);
106         checkPort(destinationPort, addrFamily);
107 
108         this.protocolVersion = protocolVersion;
109         this.command = command;
110         this.proxiedProtocol = proxiedProtocol;
111         this.sourceAddress = sourceAddress;
112         this.destinationAddress = destinationAddress;
113         this.sourcePort = sourcePort;
114         this.destinationPort = destinationPort;
115         this.tlvs = Collections.unmodifiableList(tlvs);
116 
117         leak = leakDetector.track(this);
118     }
119 
120     /**
121      * Decodes a version 2, binary proxy protocol header.
122      *
123      * @param header                     a version 2 proxy protocol header
124      * @return                           {@link HAProxyMessage} instance
125      * @throws HAProxyProtocolException  if any portion of the header is invalid
126      */
127     static HAProxyMessage decodeHeader(ByteBuf header) {
128         ObjectUtil.checkNotNull(header, "header");
129 
130         if (header.readableBytes() < 16) {
131             throw new HAProxyProtocolException(
132                     "incomplete header: " + header.readableBytes() + " bytes (expected: 16+ bytes)");
133         }
134 
135         // Per spec, the 13th byte is the protocol version and command byte
136         header.skipBytes(12);
137         final byte verCmdByte = header.readByte();
138 
139         HAProxyProtocolVersion ver;
140         try {
141             ver = HAProxyProtocolVersion.valueOf(verCmdByte);
142         } catch (IllegalArgumentException e) {
143             throw new HAProxyProtocolException(e);
144         }
145 
146         if (ver != HAProxyProtocolVersion.V2) {
147             throw new HAProxyProtocolException("version 1 unsupported: 0x" + Integer.toHexString(verCmdByte));
148         }
149 
150         HAProxyCommand cmd;
151         try {
152             cmd = HAProxyCommand.valueOf(verCmdByte);
153         } catch (IllegalArgumentException e) {
154             throw new HAProxyProtocolException(e);
155         }
156 
157         if (cmd == HAProxyCommand.LOCAL) {
158             return unknownMsg(HAProxyProtocolVersion.V2, HAProxyCommand.LOCAL);
159         }
160 
161         // Per spec, the 14th byte is the protocol and address family byte
162         HAProxyProxiedProtocol protAndFam;
163         try {
164             protAndFam = HAProxyProxiedProtocol.valueOf(header.readByte());
165         } catch (IllegalArgumentException e) {
166             throw new HAProxyProtocolException(e);
167         }
168 
169         if (protAndFam == HAProxyProxiedProtocol.UNKNOWN) {
170             return unknownMsg(HAProxyProtocolVersion.V2, HAProxyCommand.PROXY);
171         }
172 
173         int addressInfoLen = header.readUnsignedShort();
174 
175         String srcAddress;
176         String dstAddress;
177         int addressLen;
178         int srcPort = 0;
179         int dstPort = 0;
180 
181         AddressFamily addressFamily = protAndFam.addressFamily();
182 
183         if (addressFamily == AddressFamily.AF_UNIX) {
184             // unix sockets require 216 bytes for address information
185             if (addressInfoLen < 216 || header.readableBytes() < 216) {
186                 throw new HAProxyProtocolException(
187                     "incomplete UNIX socket address information: " +
188                             Math.min(addressInfoLen, header.readableBytes()) + " bytes (expected: 216+ bytes)");
189             }
190             int startIdx = header.readerIndex();
191             int addressEnd = header.indexOf(startIdx, startIdx + 108, (byte) 0); // FIND_NUL
192             if (addressEnd == -1) {
193                 addressLen = 108;
194             } else {
195                 addressLen = addressEnd - startIdx;
196             }
197             srcAddress = header.toString(startIdx, addressLen, CharsetUtil.US_ASCII);
198 
199             startIdx += 108;
200 
201             addressEnd = header.indexOf(startIdx, startIdx + 108, (byte) 0); // FIND_NUL
202             if (addressEnd == -1) {
203                 addressLen = 108;
204             } else {
205                 addressLen = addressEnd - startIdx;
206             }
207             dstAddress = header.toString(startIdx, addressLen, CharsetUtil.US_ASCII);
208             // AF_UNIX defines that exactly 108 bytes are reserved for the address. The previous methods
209             // did not increase the reader index although we already consumed the information.
210             header.readerIndex(startIdx + 108);
211         } else {
212             if (addressFamily == AddressFamily.AF_IPv4) {
213                 // IPv4 requires 12 bytes for address information
214                 if (addressInfoLen < 12 || header.readableBytes() < 12) {
215                     throw new HAProxyProtocolException(
216                         "incomplete IPv4 address information: " +
217                                 Math.min(addressInfoLen, header.readableBytes()) + " bytes (expected: 12+ bytes)");
218                 }
219                 addressLen = 4;
220             } else if (addressFamily == AddressFamily.AF_IPv6) {
221                 // IPv6 requires 36 bytes for address information
222                 if (addressInfoLen < 36 || header.readableBytes() < 36) {
223                     throw new HAProxyProtocolException(
224                         "incomplete IPv6 address information: " +
225                                 Math.min(addressInfoLen, header.readableBytes()) + " bytes (expected: 36+ bytes)");
226                 }
227                 addressLen = 16;
228             } else {
229                 throw new HAProxyProtocolException(
230                     "unable to parse address information (unknown address family: " + addressFamily + ')');
231             }
232 
233             // Per spec, the src address begins at the 17th byte
234             srcAddress = ipBytesToString(header, addressLen);
235             dstAddress = ipBytesToString(header, addressLen);
236             srcPort = header.readUnsignedShort();
237             dstPort = header.readUnsignedShort();
238         }
239 
240         final List<HAProxyTLV> tlvs = readTlvs(header);
241 
242         return new HAProxyMessage(ver, cmd, protAndFam, srcAddress, dstAddress, srcPort, dstPort, tlvs);
243     }
244 
245     private static List<HAProxyTLV> readTlvs(final ByteBuf header) {
246         HAProxyTLV haProxyTLV = readNextTLV(header, 0);
247         if (haProxyTLV == null) {
248             return Collections.emptyList();
249         }
250         // In most cases there are less than 4 TLVs available
251         List<HAProxyTLV> haProxyTLVs = new ArrayList<HAProxyTLV>(4);
252 
253         try {
254             do {
255                 haProxyTLVs.add(haProxyTLV);
256                 if (haProxyTLV instanceof HAProxySSLTLV) {
257                     haProxyTLVs.addAll(((HAProxySSLTLV) haProxyTLV).encapsulatedTLVs());
258                 }
259             } while ((haProxyTLV = readNextTLV(header, 0)) != null);
260         } catch (Throwable t) {
261             // Release all previously read TLVs before rethrowing as otherwise we would leak.
262             releaseTlvs(haProxyTLVs);
263             throw t;
264         }
265         return haProxyTLVs;
266     }
267 
268     private static void releaseDeep(List<HAProxyTLV> children) {
269         for (HAProxyTLV child : children) {
270             child.release();
271             if (child instanceof HAProxySSLTLV) {
272                 releaseDeep(((HAProxySSLTLV) child).encapsulatedTLVs());
273             }
274         }
275     }
276 
277     private static void releaseTlvs(List<HAProxyTLV> tlvs) {
278         int skip = 0;
279         for (HAProxyTLV tlv : tlvs) {
280             if (skip > 0) {
281                 skip--;
282                 // This TLV is a flattened depth-1 child. If it encapsulates anything (depth-2+),
283                 // those deeper children were NOT flattened, so we must release them recursively.
284                 if (tlv instanceof HAProxySSLTLV) {
285                     releaseDeep(((HAProxySSLTLV) tlv).encapsulatedTLVs());
286                 }
287             } else if (tlv instanceof HAProxySSLTLV) {
288                 // This is a top-level (depth-0) SSL TLV.
289                 // Its immediate children (depth-1) were flattened into this list,
290                 // so we must skip them in the outer loop to avoid treating them as top-level TLVs.
291                 skip = ((HAProxySSLTLV) tlv).encapsulatedTLVs().size();
292             }
293             tlv.release();
294         }
295     }
296 
297     private static HAProxyTLV readNextTLV(final ByteBuf header, int nestingLevel) {
298         if (nestingLevel > MAX_NESTING_LEVEL) {
299             throw new HAProxyProtocolException(
300                     "Maximum TLV nesting level reached: " + nestingLevel + " (expected: < " + MAX_NESTING_LEVEL + ')');
301         }
302         // We need at least 4 bytes for a TLV
303         if (header.readableBytes() < 4) {
304             return null;
305         }
306 
307         final byte typeAsByte = header.readByte();
308         final HAProxyTLV.Type type = HAProxyTLV.Type.typeForByteValue(typeAsByte);
309 
310         final int length = header.readUnsignedShort();
311         switch (type) {
312         case PP2_TYPE_SSL:
313             if (length < 5) {
314                 throw new HAProxyProtocolException("TLV length must be at least 5 but was: " + length);
315             }
316             if (length > header.readableBytes()) {
317                 throw new HAProxyProtocolException("TLV length must be smaller or equal the readable bytes (" +
318                         header.readableBytes() + ") but was: " + length);
319             }
320             // Slice the rawContent but only retain it if we didn't see an error as otherwise we might
321             // leak.
322             final ByteBuf rawContent = header.slice(header.readerIndex(), length);
323             final ByteBuf byteBuf = header.readSlice(length);
324             final byte client = byteBuf.readByte();
325             final int verify = byteBuf.readInt();
326 
327             if (byteBuf.readableBytes() >= 4) {
328 
329                 final List<HAProxyTLV> encapsulatedTlvs = new ArrayList<HAProxyTLV>(4);
330                 try {
331                     do {
332                         final HAProxyTLV haProxyTLV = readNextTLV(byteBuf, nestingLevel + 1);
333                         if (haProxyTLV == null) {
334                             break;
335                         }
336                         encapsulatedTlvs.add(haProxyTLV);
337                     } while (byteBuf.readableBytes() >= 4);
338                 }  catch (Throwable t) {
339                     // Release all previously read TLVs before rethrowing as otherwise we would leak.
340                     releaseTlvs(encapsulatedTlvs);
341                     throw t;
342                 }
343 
344                 return new HAProxySSLTLV(verify, client, encapsulatedTlvs, rawContent.retain());
345             }
346             return new HAProxySSLTLV(verify, client, Collections.<HAProxyTLV>emptyList(), rawContent.retain());
347         // If we're not dealing with an SSL Type, we can use the same mechanism
348         case PP2_TYPE_ALPN:
349         case PP2_TYPE_AUTHORITY:
350         case PP2_TYPE_SSL_VERSION:
351         case PP2_TYPE_SSL_CN:
352         case PP2_TYPE_NETNS:
353         case OTHER:
354             return new HAProxyTLV(type, typeAsByte, header.readRetainedSlice(length));
355         default:
356             return null;
357         }
358     }
359 
360     /**
361      * Decodes a version 1, human-readable proxy protocol header.
362      *
363      * @param header                     a version 1 proxy protocol header
364      * @return                           {@link HAProxyMessage} instance
365      * @throws HAProxyProtocolException  if any portion of the header is invalid
366      */
367     static HAProxyMessage decodeHeader(String header) {
368         if (header == null) {
369             throw new HAProxyProtocolException("header");
370         }
371 
372         String[] parts = header.split(" ");
373         int numParts = parts.length;
374 
375         if (numParts < 2) {
376             throw new HAProxyProtocolException(
377                     "invalid header: " + header + " (expected: 'PROXY' and proxied protocol values)");
378         }
379 
380         if (!"PROXY".equals(parts[0])) {
381             throw new HAProxyProtocolException("unknown identifier: " + parts[0]);
382         }
383 
384         HAProxyProxiedProtocol protAndFam;
385         try {
386             protAndFam = HAProxyProxiedProtocol.valueOf(parts[1]);
387         } catch (IllegalArgumentException e) {
388             throw new HAProxyProtocolException(e);
389         }
390 
391         if (protAndFam != HAProxyProxiedProtocol.TCP4 &&
392                 protAndFam != HAProxyProxiedProtocol.TCP6 &&
393                 protAndFam != HAProxyProxiedProtocol.UNKNOWN) {
394             throw new HAProxyProtocolException("unsupported v1 proxied protocol: " + parts[1]);
395         }
396 
397         if (protAndFam == HAProxyProxiedProtocol.UNKNOWN) {
398             return unknownMsg(HAProxyProtocolVersion.V1, HAProxyCommand.PROXY);
399         }
400 
401         if (numParts != 6) {
402             throw new HAProxyProtocolException("invalid TCP4/6 header: " + header + " (expected: 6 parts)");
403         }
404 
405         try {
406             return new HAProxyMessage(
407                     HAProxyProtocolVersion.V1, HAProxyCommand.PROXY,
408                     protAndFam, parts[2], parts[3], parts[4], parts[5]);
409         } catch (RuntimeException e) {
410             throw new HAProxyProtocolException("invalid HAProxy message", e);
411         }
412     }
413 
414     /**
415      * Proxy protocol message for 'UNKNOWN' proxied protocols. Per spec, when the proxied protocol is
416      * 'UNKNOWN' we must discard all other header values.
417      */
418     private static HAProxyMessage unknownMsg(HAProxyProtocolVersion version, HAProxyCommand command) {
419         return new HAProxyMessage(version, command, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0);
420     }
421 
422     /**
423      * Convert ip address bytes to string representation
424      *
425      * @param header     buffer containing ip address bytes
426      * @param addressLen number of bytes to read (4 bytes for IPv4, 16 bytes for IPv6)
427      * @return           string representation of the ip address
428      */
429     private static String ipBytesToString(ByteBuf header, int addressLen) {
430         StringBuilder sb = new StringBuilder();
431         final int ipv4Len = 4;
432         final int ipv6Len = 8;
433         if (addressLen == ipv4Len) {
434             for (int i = 0; i < ipv4Len; i++) {
435                 sb.append(header.readByte() & 0xff);
436                 sb.append('.');
437             }
438         } else {
439             for (int i = 0; i < ipv6Len; i++) {
440                 sb.append(Integer.toHexString(header.readUnsignedShort()));
441                 sb.append(':');
442             }
443         }
444         sb.setLength(sb.length() - 1);
445         return sb.toString();
446     }
447 
448     /**
449      * Convert port to integer
450      *
451      * @param value                      the port
452      * @return                           port as an integer
453      * @throws IllegalArgumentException  if port is not a valid integer
454      */
455     private static int portStringToInt(String value) {
456         int port;
457         try {
458             port = Integer.parseInt(value);
459         } catch (NumberFormatException e) {
460             throw new IllegalArgumentException("invalid port: " + value, e);
461         }
462 
463         if (port <= 0 || port > 65535) {
464             throw new IllegalArgumentException("invalid port: " + value + " (expected: 1 ~ 65535)");
465         }
466 
467         return port;
468     }
469 
470     /**
471      * Validate an address (IPv4, IPv6, Unix Socket)
472      *
473      * @param address                    human-readable address
474      * @param addrFamily                 the {@link AddressFamily} to check the address against
475      * @throws IllegalArgumentException  if the address is invalid
476      */
477     private static void checkAddress(String address, AddressFamily addrFamily) {
478         ObjectUtil.checkNotNull(addrFamily, "addrFamily");
479 
480         switch (addrFamily) {
481             case AF_UNSPEC:
482                 if (address != null) {
483                     throw new IllegalArgumentException("unable to validate an AF_UNSPEC address: " + address);
484                 }
485                 return;
486             case AF_UNIX:
487                 ObjectUtil.checkNotNull(address, "address");
488                 if (address.getBytes(CharsetUtil.US_ASCII).length > 108) {
489                     throw new IllegalArgumentException("invalid AF_UNIX address: " + address);
490                 }
491                 return;
492         }
493 
494         ObjectUtil.checkNotNull(address, "address");
495 
496         switch (addrFamily) {
497             case AF_IPv4:
498                 if (!NetUtil.isValidIpV4Address(address)) {
499                     throw new IllegalArgumentException("invalid IPv4 address: " + address);
500                 }
501                 break;
502             case AF_IPv6:
503                 if (!NetUtil.isValidIpV6Address(address)) {
504                     throw new IllegalArgumentException("invalid IPv6 address: " + address);
505                 }
506                 break;
507             default:
508                 throw new IllegalArgumentException("unexpected addrFamily: " + addrFamily);
509         }
510     }
511 
512     /**
513      * Validate the port depending on the addrFamily.
514      *
515      * @param port                       the UDP/TCP port
516      * @throws IllegalArgumentException  if the port is out of range (0-65535 inclusive)
517      */
518     private static void checkPort(int port, AddressFamily addrFamily) {
519         switch (addrFamily) {
520         case AF_IPv6:
521         case AF_IPv4:
522             if (port < 0 || port > 65535) {
523                 throw new IllegalArgumentException("invalid port: " + port + " (expected: 0 ~ 65535)");
524             }
525             break;
526         case AF_UNIX:
527         case AF_UNSPEC:
528             if (port != 0) {
529                 throw new IllegalArgumentException("port cannot be specified with addrFamily: " + addrFamily);
530             }
531             break;
532         default:
533             throw new IllegalArgumentException("unexpected addrFamily: " + addrFamily);
534         }
535     }
536 
537     /**
538      * Returns the {@link HAProxyProtocolVersion} of this {@link HAProxyMessage}.
539      */
540     public HAProxyProtocolVersion protocolVersion() {
541         return protocolVersion;
542     }
543 
544     /**
545      * Returns the {@link HAProxyCommand} of this {@link HAProxyMessage}.
546      */
547     public HAProxyCommand command() {
548         return command;
549     }
550 
551     /**
552      * Returns the {@link HAProxyProxiedProtocol} of this {@link HAProxyMessage}.
553      */
554     public HAProxyProxiedProtocol proxiedProtocol() {
555         return proxiedProtocol;
556     }
557 
558     /**
559      * Returns the human-readable source address of this {@link HAProxyMessage} or {@code null}
560      * if HAProxy performs health check with {@code send-proxy-v2}.
561      */
562     public String sourceAddress() {
563         return sourceAddress;
564     }
565 
566     /**
567      * Returns the human-readable destination address of this {@link HAProxyMessage}.
568      */
569     public String destinationAddress() {
570         return destinationAddress;
571     }
572 
573     /**
574      * Returns the UDP/TCP source port of this {@link HAProxyMessage}.
575      */
576     public int sourcePort() {
577         return sourcePort;
578     }
579 
580     /**
581      * Returns the UDP/TCP destination port of this {@link HAProxyMessage}.
582      */
583     public int destinationPort() {
584         return destinationPort;
585     }
586 
587     /**
588      * Returns a list of {@link HAProxyTLV} or an empty list if no TLVs are present.
589      * <p>
590      * TLVs are only available for the Proxy Protocol V2
591      */
592     public List<HAProxyTLV> tlvs() {
593         return tlvs;
594     }
595 
596     int tlvNumBytes() {
597         int tlvNumBytes = 0;
598         for (int i = 0; i < tlvs.size(); i++) {
599             tlvNumBytes += tlvs.get(i).totalNumBytes();
600         }
601         return tlvNumBytes;
602     }
603 
604     @Override
605     public HAProxyMessage touch() {
606         tryRecord();
607         return (HAProxyMessage) super.touch();
608     }
609 
610     @Override
611     public HAProxyMessage touch(Object hint) {
612         if (leak != null) {
613             leak.record(hint);
614         }
615         return this;
616     }
617 
618     @Override
619     public HAProxyMessage retain() {
620         tryRecord();
621         return (HAProxyMessage) super.retain();
622     }
623 
624     @Override
625     public HAProxyMessage retain(int increment) {
626         tryRecord();
627         return (HAProxyMessage) super.retain(increment);
628     }
629 
630     @Override
631     public boolean release() {
632         tryRecord();
633         return super.release();
634     }
635 
636     @Override
637     public boolean release(int decrement) {
638         tryRecord();
639         return super.release(decrement);
640     }
641 
642     private void tryRecord() {
643         if (leak != null) {
644             leak.record();
645         }
646     }
647 
648     @Override
649     protected void deallocate() {
650         try {
651             releaseTlvs(tlvs);
652         } finally {
653             final ResourceLeakTracker<HAProxyMessage> leak = this.leak;
654             if (leak != null) {
655                 boolean closed = leak.close(this);
656                 assert closed;
657             }
658         }
659     }
660 
661     @Override
662     public String toString() {
663         StringBuilder sb = new StringBuilder(256)
664                 .append(StringUtil.simpleClassName(this))
665                 .append("(protocolVersion: ").append(protocolVersion)
666                 .append(", command: ").append(command)
667                 .append(", proxiedProtocol: ").append(proxiedProtocol)
668                 .append(", sourceAddress: ").append(sourceAddress)
669                 .append(", destinationAddress: ").append(destinationAddress)
670                 .append(", sourcePort: ").append(sourcePort)
671                 .append(", destinationPort: ").append(destinationPort)
672                 .append(", tlvs: [");
673         if (!tlvs.isEmpty()) {
674             for (HAProxyTLV tlv: tlvs) {
675                 sb.append(tlv).append(", ");
676             }
677             sb.setLength(sb.length() - 2);
678         }
679         sb.append("])");
680         return sb.toString();
681     }
682 }