View Javadoc
1   /*
2    * Copyright 2016 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  
17  package io.netty.util.internal;
18  
19  import io.netty.util.NetUtil;
20  import io.netty.util.internal.logging.InternalLogger;
21  import io.netty.util.internal.logging.InternalLoggerFactory;
22  
23  import java.net.InetAddress;
24  import java.net.NetworkInterface;
25  import java.net.SocketException;
26  import java.util.Arrays;
27  import java.util.Enumeration;
28  import java.util.LinkedHashMap;
29  import java.util.Map;
30  import java.util.Map.Entry;
31  
32  import static io.netty.util.internal.EmptyArrays.EMPTY_BYTES;
33  
34  public final class MacAddressUtil {
35      private static final InternalLogger logger = InternalLoggerFactory.getInstance(MacAddressUtil.class);
36  
37      private static final int EUI64_MAC_ADDRESS_LENGTH = 8;
38      private static final int EUI48_MAC_ADDRESS_LENGTH = 6;
39  
40      /**
41       * Obtains the best MAC address found on local network interfaces.
42       * Generally speaking, an active network interface used on public
43       * networks is better than a local network interface.
44       *
45       * @return byte array containing a MAC. null if no MAC can be found.
46       */
47      public static byte[] bestAvailableMac() {
48          // Find the best MAC address available.
49          byte[] bestMacAddr = EMPTY_BYTES;
50          InetAddress bestInetAddr = NetUtil.LOCALHOST4;
51  
52          // Retrieve the list of available network interfaces.
53          Map<NetworkInterface, InetAddress> ifaces = new LinkedHashMap<NetworkInterface, InetAddress>();
54          try {
55              Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
56              if (interfaces != null) {
57                  while (interfaces.hasMoreElements()) {
58                      NetworkInterface iface = interfaces.nextElement();
59                      // Use the interface with proper INET addresses only.
60                      Enumeration<InetAddress> addrs = SocketUtils.addressesFromNetworkInterface(iface);
61                      if (addrs.hasMoreElements()) {
62                          InetAddress a = addrs.nextElement();
63                          if (!a.isLoopbackAddress()) {
64                              ifaces.put(iface, a);
65                          }
66                      }
67                  }
68              }
69          } catch (SocketException e) {
70              logger.warn("Failed to retrieve the list of available network interfaces", e);
71          }
72  
73          for (Entry<NetworkInterface, InetAddress> entry: ifaces.entrySet()) {
74              NetworkInterface iface = entry.getKey();
75              InetAddress inetAddr = entry.getValue();
76              if (iface.isVirtual()) {
77                  continue;
78              }
79  
80              byte[] macAddr;
81              try {
82                  macAddr = SocketUtils.hardwareAddressFromNetworkInterface(iface);
83              } catch (SocketException e) {
84                  logger.debug("Failed to get the hardware address of a network interface: {}", iface, e);
85                  continue;
86              }
87  
88              boolean replace = false;
89              int res = compareAddresses(bestMacAddr, macAddr);
90              if (res < 0) {
91                  // Found a better MAC address.
92                  replace = true;
93              } else if (res == 0) {
94                  // Two MAC addresses are of pretty much same quality.
95                  res = compareAddresses(bestInetAddr, inetAddr);
96                  if (res < 0) {
97                      // Found a MAC address with better INET address.
98                      replace = true;
99                  } else if (res == 0) {
100                     // Cannot tell the difference.  Choose the longer one.
101                     if (bestMacAddr.length < macAddr.length) {
102                         replace = true;
103                     }
104                 }
105             }
106 
107             if (replace) {
108                 bestMacAddr = macAddr;
109                 bestInetAddr = inetAddr;
110             }
111         }
112 
113         if (bestMacAddr == EMPTY_BYTES) {
114             return null;
115         }
116 
117         if (bestMacAddr.length == EUI48_MAC_ADDRESS_LENGTH) { // EUI-48 - convert to EUI-64
118             byte[] newAddr = new byte[EUI64_MAC_ADDRESS_LENGTH];
119             System.arraycopy(bestMacAddr, 0, newAddr, 0, 3);
120             newAddr[3] = (byte) 0xFF;
121             newAddr[4] = (byte) 0xFE;
122             System.arraycopy(bestMacAddr, 3, newAddr, 5, 3);
123             bestMacAddr = newAddr;
124         } else {
125             // Unknown
126             bestMacAddr = Arrays.copyOf(bestMacAddr, EUI64_MAC_ADDRESS_LENGTH);
127         }
128 
129         return bestMacAddr;
130     }
131 
132     /**
133      * Returns the result of {@link #bestAvailableMac()} if non-{@code null} otherwise returns a random EUI-64 MAC
134      * address.
135      */
136     public static byte[] defaultMachineId() {
137         byte[] bestMacAddr = bestAvailableMac();
138         if (bestMacAddr == null) {
139             bestMacAddr = new byte[EUI64_MAC_ADDRESS_LENGTH];
140             PlatformDependent.threadLocalRandom().nextBytes(bestMacAddr);
141             logger.warn(
142                     "Failed to find a usable hardware address from the network interfaces; using random bytes: {}",
143                     formatAddress(bestMacAddr));
144         }
145         return bestMacAddr;
146     }
147 
148     /**
149      * Parse a EUI-48, MAC-48, or EUI-64 MAC address from a {@link String} and return it as a {@code byte[]}.
150      * @param value The string representation of the MAC address.
151      * @return The byte representation of the MAC address.
152      */
153     public static byte[] parseMAC(String value) {
154         final byte[] machineId;
155         final char separator;
156         switch (value.length()) {
157             case 17:
158                 separator = value.charAt(2);
159                 validateMacSeparator(separator);
160                 machineId = new byte[EUI48_MAC_ADDRESS_LENGTH];
161                 break;
162             case 23:
163                 separator = value.charAt(2);
164                 validateMacSeparator(separator);
165                 machineId = new byte[EUI64_MAC_ADDRESS_LENGTH];
166                 break;
167             default:
168                 throw new IllegalArgumentException("value is not supported [MAC-48, EUI-48, EUI-64]");
169         }
170 
171         final int end = machineId.length - 1;
172         int j = 0;
173         for (int i = 0; i < end; ++i, j += 3) {
174             final int sIndex = j + 2;
175             machineId[i] = StringUtil.decodeHexByte(value, j);
176             if (value.charAt(sIndex) != separator) {
177                 throw new IllegalArgumentException("expected separator '" + separator + " but got '" +
178                         value.charAt(sIndex) + "' at index: " + sIndex);
179             }
180         }
181 
182         machineId[end] = StringUtil.decodeHexByte(value, j);
183 
184         return machineId;
185     }
186 
187     private static void validateMacSeparator(char separator) {
188         if (separator != ':' && separator != '-') {
189             throw new IllegalArgumentException("unsupported separator: " + separator + " (expected: [:-])");
190         }
191     }
192 
193     /**
194      * @param addr byte array of a MAC address.
195      * @return hex formatted MAC address.
196      */
197     public static String formatAddress(byte[] addr) {
198         StringBuilder buf = new StringBuilder(24);
199         for (byte b: addr) {
200             buf.append(String.format("%02x:", b & 0xff));
201         }
202         return buf.substring(0, buf.length() - 1);
203     }
204 
205     /**
206      * @return positive - current is better, 0 - cannot tell from MAC addr, negative - candidate is better.
207      */
208     // visible for testing
209     static int compareAddresses(byte[] current, byte[] candidate) {
210         if (candidate == null || candidate.length < EUI48_MAC_ADDRESS_LENGTH) {
211             return 1;
212         }
213 
214         // Must not be filled with only 0 and 1.
215         boolean onlyZeroAndOne = true;
216         for (byte b: candidate) {
217             if (b != 0 && b != 1) {
218                 onlyZeroAndOne = false;
219                 break;
220             }
221         }
222 
223         if (onlyZeroAndOne) {
224             return 1;
225         }
226 
227         // Must not be a multicast address
228         if ((candidate[0] & 1) != 0) {
229             return 1;
230         }
231 
232         // Prefer globally unique address.
233         if ((candidate[0] & 2) == 0) {
234             if (current.length != 0 && (current[0] & 2) == 0) {
235                 // Both current and candidate are globally unique addresses.
236                 return 0;
237             } else {
238                 // Only candidate is globally unique.
239                 return -1;
240             }
241         } else {
242             if (current.length != 0 && (current[0] & 2) == 0) {
243                 // Only current is globally unique.
244                 return 1;
245             } else {
246                 // Both current and candidate are non-unique.
247                 return 0;
248             }
249         }
250     }
251 
252     /**
253      * @return positive - current is better, 0 - cannot tell, negative - candidate is better
254      */
255     private static int compareAddresses(InetAddress current, InetAddress candidate) {
256         return scoreAddress(current) - scoreAddress(candidate);
257     }
258 
259     private static int scoreAddress(InetAddress addr) {
260         if (addr.isAnyLocalAddress() || addr.isLoopbackAddress()) {
261             return 0;
262         }
263         if (addr.isMulticastAddress()) {
264             return 1;
265         }
266         if (addr.isLinkLocalAddress()) {
267             return 2;
268         }
269         if (addr.isSiteLocalAddress()) {
270             return 3;
271         }
272 
273         return 4;
274     }
275 
276     private MacAddressUtil() { }
277 }