IPv6.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. <?php
  2. namespace IPLib\Address;
  3. use IPLib\ParseStringFlag;
  4. use IPLib\Range\RangeInterface;
  5. use IPLib\Range\Subnet;
  6. use IPLib\Range\Type as RangeType;
  7. /**
  8. * An IPv6 address.
  9. */
  10. class IPv6 implements AddressInterface
  11. {
  12. /**
  13. * The long string representation of the address.
  14. *
  15. * @var string
  16. *
  17. * @example '0000:0000:0000:0000:0000:0000:0000:0001'
  18. */
  19. protected $longAddress;
  20. /**
  21. * The long string representation of the address.
  22. *
  23. * @var string|null
  24. *
  25. * @example '::1'
  26. */
  27. protected $shortAddress;
  28. /**
  29. * The byte list of the IP address.
  30. *
  31. * @var int[]|null
  32. */
  33. protected $bytes;
  34. /**
  35. * The word list of the IP address.
  36. *
  37. * @var int[]|null
  38. */
  39. protected $words;
  40. /**
  41. * The type of the range of this IP address.
  42. *
  43. * @var int|null
  44. */
  45. protected $rangeType;
  46. /**
  47. * An array containing RFC designated address ranges.
  48. *
  49. * @var array|null
  50. */
  51. private static $reservedRanges;
  52. /**
  53. * Initializes the instance.
  54. *
  55. * @param string $longAddress
  56. */
  57. public function __construct($longAddress)
  58. {
  59. $this->longAddress = $longAddress;
  60. $this->shortAddress = null;
  61. $this->bytes = null;
  62. $this->words = null;
  63. $this->rangeType = null;
  64. }
  65. /**
  66. * {@inheritdoc}
  67. *
  68. * @see \IPLib\Address\AddressInterface::__toString()
  69. */
  70. public function __toString()
  71. {
  72. return $this->toString();
  73. }
  74. /**
  75. * {@inheritdoc}
  76. *
  77. * @see \IPLib\Address\AddressInterface::getNumberOfBits()
  78. */
  79. public static function getNumberOfBits()
  80. {
  81. return 128;
  82. }
  83. /**
  84. * @deprecated since 1.17.0: use the parseString() method instead.
  85. * For upgrading:
  86. * - if $mayIncludePort is true, use the ParseStringFlag::MAY_INCLUDE_PORT flag
  87. * - if $mayIncludeZoneID is true, use the ParseStringFlag::MAY_INCLUDE_ZONEID flag
  88. *
  89. * @param string|mixed $address
  90. * @param bool $mayIncludePort
  91. * @param bool $mayIncludeZoneID
  92. *
  93. * @return static|null
  94. *
  95. * @see \IPLib\Address\IPv6::parseString()
  96. * @since 1.1.0 added the $mayIncludePort argument
  97. * @since 1.3.0 added the $mayIncludeZoneID argument
  98. */
  99. public static function fromString($address, $mayIncludePort = true, $mayIncludeZoneID = true)
  100. {
  101. return static::parseString($address, 0 | ($mayIncludePort ? ParseStringFlag::MAY_INCLUDE_PORT : 0) | ($mayIncludeZoneID ? ParseStringFlag::MAY_INCLUDE_ZONEID : 0));
  102. }
  103. /**
  104. * Parse a string and returns an IPv6 instance if the string is valid, or null otherwise.
  105. *
  106. * @param string|mixed $address the address to parse
  107. * @param int $flags A combination or zero or more flags
  108. *
  109. * @return static|null
  110. *
  111. * @see \IPLib\ParseStringFlag
  112. * @since 1.17.0
  113. */
  114. public static function parseString($address, $flags = 0)
  115. {
  116. if (!is_string($address)) {
  117. return null;
  118. }
  119. $matches = null;
  120. $flags = (int) $flags;
  121. if ($flags & ParseStringFlag::ADDRESS_MAYBE_RDNS) {
  122. if (preg_match('/^([0-9a-f](?:\.[0-9a-f]){31})\.ip6\.arpa\.?/i', $address, $matches)) {
  123. $nibbles = array_reverse(explode('.', $matches[1]));
  124. $quibbles = array();
  125. foreach (array_chunk($nibbles, 4) as $n) {
  126. $quibbles[] = implode('', $n);
  127. }
  128. $address = implode(':', $quibbles);
  129. }
  130. }
  131. $result = null;
  132. if (is_string($address) && strpos($address, ':') !== false && strpos($address, ':::') === false) {
  133. if ($flags & ParseStringFlag::MAY_INCLUDE_PORT && $address[0] === '[' && preg_match('/^\[(.+)]:\d+$/', $address, $matches)) {
  134. $address = $matches[1];
  135. }
  136. if ($flags & ParseStringFlag::MAY_INCLUDE_ZONEID) {
  137. $percentagePos = strpos($address, '%');
  138. if ($percentagePos > 0) {
  139. $address = substr($address, 0, $percentagePos);
  140. }
  141. }
  142. if (preg_match('/^((?:[0-9a-f]*:+)+)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i', $address, $matches)) {
  143. $address6 = static::parseString($matches[1] . '0:0');
  144. if ($address6 !== null) {
  145. $address4 = IPv4::parseString($matches[2]);
  146. if ($address4 !== null) {
  147. $bytes4 = $address4->getBytes();
  148. $address6->longAddress = substr($address6->longAddress, 0, -9) . sprintf('%02x%02x:%02x%02x', $bytes4[0], $bytes4[1], $bytes4[2], $bytes4[3]);
  149. $result = $address6;
  150. }
  151. }
  152. } else {
  153. if (strpos($address, '::') === false) {
  154. $chunks = explode(':', $address);
  155. } else {
  156. $chunks = array();
  157. $parts = explode('::', $address);
  158. if (count($parts) === 2) {
  159. $before = ($parts[0] === '') ? array() : explode(':', $parts[0]);
  160. $after = ($parts[1] === '') ? array() : explode(':', $parts[1]);
  161. $missing = 8 - count($before) - count($after);
  162. if ($missing >= 0) {
  163. $chunks = $before;
  164. if ($missing !== 0) {
  165. $chunks = array_merge($chunks, array_fill(0, $missing, '0'));
  166. }
  167. $chunks = array_merge($chunks, $after);
  168. }
  169. }
  170. }
  171. if (count($chunks) === 8) {
  172. $nums = array_map(
  173. function ($chunk) {
  174. return preg_match('/^[0-9A-Fa-f]{1,4}$/', $chunk) ? hexdec($chunk) : false;
  175. },
  176. $chunks
  177. );
  178. if (!in_array(false, $nums, true)) {
  179. $longAddress = implode(
  180. ':',
  181. array_map(
  182. function ($num) {
  183. return sprintf('%04x', $num);
  184. },
  185. $nums
  186. )
  187. );
  188. $result = new static($longAddress);
  189. }
  190. }
  191. }
  192. }
  193. return $result;
  194. }
  195. /**
  196. * Parse an array of bytes and returns an IPv6 instance if the array is valid, or null otherwise.
  197. *
  198. * @param int[]|array $bytes
  199. *
  200. * @return static|null
  201. */
  202. public static function fromBytes(array $bytes)
  203. {
  204. $result = null;
  205. if (count($bytes) === 16) {
  206. $address = '';
  207. for ($i = 0; $i < 16; $i++) {
  208. if ($i !== 0 && $i % 2 === 0) {
  209. $address .= ':';
  210. }
  211. $byte = $bytes[$i];
  212. if (is_int($byte) && $byte >= 0 && $byte <= 255) {
  213. $address .= sprintf('%02x', $byte);
  214. } else {
  215. $address = null;
  216. break;
  217. }
  218. }
  219. if ($address !== null) {
  220. $result = new static($address);
  221. }
  222. }
  223. return $result;
  224. }
  225. /**
  226. * Parse an array of words and returns an IPv6 instance if the array is valid, or null otherwise.
  227. *
  228. * @param int[]|array $words
  229. *
  230. * @return static|null
  231. */
  232. public static function fromWords(array $words)
  233. {
  234. $result = null;
  235. if (count($words) === 8) {
  236. $chunks = array();
  237. for ($i = 0; $i < 8; $i++) {
  238. $word = $words[$i];
  239. if (is_int($word) && $word >= 0 && $word <= 0xffff) {
  240. $chunks[] = sprintf('%04x', $word);
  241. } else {
  242. $chunks = null;
  243. break;
  244. }
  245. }
  246. if ($chunks !== null) {
  247. $result = new static(implode(':', $chunks));
  248. }
  249. }
  250. return $result;
  251. }
  252. /**
  253. * {@inheritdoc}
  254. *
  255. * @see \IPLib\Address\AddressInterface::toString()
  256. */
  257. public function toString($long = false)
  258. {
  259. if ($long) {
  260. $result = $this->longAddress;
  261. } else {
  262. if ($this->shortAddress === null) {
  263. if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) {
  264. $lastBytes = array_slice($this->getBytes(), -4);
  265. $this->shortAddress = '::ffff:' . implode('.', $lastBytes);
  266. } else {
  267. $chunks = array_map(
  268. function ($word) {
  269. return dechex($word);
  270. },
  271. $this->getWords()
  272. );
  273. $shortAddress = implode(':', $chunks);
  274. $matches = null;
  275. for ($i = 8; $i > 1; $i--) {
  276. $search = '(?:^|:)' . rtrim(str_repeat('0:', $i), ':') . '(?:$|:)';
  277. if (preg_match('/^(.*?)' . $search . '(.*)$/', $shortAddress, $matches)) {
  278. $shortAddress = $matches[1] . '::' . $matches[2];
  279. break;
  280. }
  281. }
  282. $this->shortAddress = $shortAddress;
  283. }
  284. }
  285. $result = $this->shortAddress;
  286. }
  287. return $result;
  288. }
  289. /**
  290. * {@inheritdoc}
  291. *
  292. * @see \IPLib\Address\AddressInterface::getBytes()
  293. */
  294. public function getBytes()
  295. {
  296. if ($this->bytes === null) {
  297. $bytes = array();
  298. foreach ($this->getWords() as $word) {
  299. $bytes[] = $word >> 8;
  300. $bytes[] = $word & 0xff;
  301. }
  302. $this->bytes = $bytes;
  303. }
  304. return $this->bytes;
  305. }
  306. /**
  307. * {@inheritdoc}
  308. *
  309. * @see \IPLib\Address\AddressInterface::getBits()
  310. */
  311. public function getBits()
  312. {
  313. $parts = array();
  314. foreach ($this->getBytes() as $byte) {
  315. $parts[] = sprintf('%08b', $byte);
  316. }
  317. return implode('', $parts);
  318. }
  319. /**
  320. * Get the word list of the IP address.
  321. *
  322. * @return int[]
  323. */
  324. public function getWords()
  325. {
  326. if ($this->words === null) {
  327. $this->words = array_map(
  328. function ($chunk) {
  329. return hexdec($chunk);
  330. },
  331. explode(':', $this->longAddress)
  332. );
  333. }
  334. return $this->words;
  335. }
  336. /**
  337. * {@inheritdoc}
  338. *
  339. * @see \IPLib\Address\AddressInterface::getAddressType()
  340. */
  341. public function getAddressType()
  342. {
  343. return Type::T_IPv6;
  344. }
  345. /**
  346. * {@inheritdoc}
  347. *
  348. * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType()
  349. */
  350. public static function getDefaultReservedRangeType()
  351. {
  352. return RangeType::T_RESERVED;
  353. }
  354. /**
  355. * {@inheritdoc}
  356. *
  357. * @see \IPLib\Address\AddressInterface::getReservedRanges()
  358. */
  359. public static function getReservedRanges()
  360. {
  361. if (self::$reservedRanges === null) {
  362. $reservedRanges = array();
  363. foreach (array(
  364. // RFC 4291
  365. '::/128' => array(RangeType::T_UNSPECIFIED),
  366. // RFC 4291
  367. '::1/128' => array(RangeType::T_LOOPBACK),
  368. // RFC 4291
  369. '100::/8' => array(RangeType::T_DISCARD, array('100::/64' => RangeType::T_DISCARDONLY)),
  370. //'2002::/16' => array(RangeType::),
  371. // RFC 4291
  372. '2000::/3' => array(RangeType::T_PUBLIC),
  373. // RFC 4193
  374. 'fc00::/7' => array(RangeType::T_PRIVATENETWORK),
  375. // RFC 4291
  376. 'fe80::/10' => array(RangeType::T_LINKLOCAL_UNICAST),
  377. // RFC 4291
  378. 'ff00::/8' => array(RangeType::T_MULTICAST),
  379. // RFC 4291
  380. //'::/8' => array(RangeType::T_RESERVED),
  381. // RFC 4048
  382. //'200::/7' => array(RangeType::T_RESERVED),
  383. // RFC 4291
  384. //'400::/6' => array(RangeType::T_RESERVED),
  385. // RFC 4291
  386. //'800::/5' => array(RangeType::T_RESERVED),
  387. // RFC 4291
  388. //'1000::/4' => array(RangeType::T_RESERVED),
  389. // RFC 4291
  390. //'4000::/3' => array(RangeType::T_RESERVED),
  391. // RFC 4291
  392. //'6000::/3' => array(RangeType::T_RESERVED),
  393. // RFC 4291
  394. //'8000::/3' => array(RangeType::T_RESERVED),
  395. // RFC 4291
  396. //'a000::/3' => array(RangeType::T_RESERVED),
  397. // RFC 4291
  398. //'c000::/3' => array(RangeType::T_RESERVED),
  399. // RFC 4291
  400. //'e000::/4' => array(RangeType::T_RESERVED),
  401. // RFC 4291
  402. //'f000::/5' => array(RangeType::T_RESERVED),
  403. // RFC 4291
  404. //'f800::/6' => array(RangeType::T_RESERVED),
  405. // RFC 4291
  406. //'fe00::/9' => array(RangeType::T_RESERVED),
  407. // RFC 3879
  408. //'fec0::/10' => array(RangeType::T_RESERVED),
  409. ) as $range => $data) {
  410. $exceptions = array();
  411. if (isset($data[1])) {
  412. foreach ($data[1] as $exceptionRange => $exceptionType) {
  413. $exceptions[] = new AssignedRange(Subnet::parseString($exceptionRange), $exceptionType);
  414. }
  415. }
  416. $reservedRanges[] = new AssignedRange(Subnet::parseString($range), $data[0], $exceptions);
  417. }
  418. self::$reservedRanges = $reservedRanges;
  419. }
  420. return self::$reservedRanges;
  421. }
  422. /**
  423. * {@inheritdoc}
  424. *
  425. * @see \IPLib\Address\AddressInterface::getRangeType()
  426. */
  427. public function getRangeType()
  428. {
  429. if ($this->rangeType === null) {
  430. $ipv4 = $this->toIPv4();
  431. if ($ipv4 !== null) {
  432. $this->rangeType = $ipv4->getRangeType();
  433. } else {
  434. $rangeType = null;
  435. foreach (static::getReservedRanges() as $reservedRange) {
  436. $rangeType = $reservedRange->getAddressType($this);
  437. if ($rangeType !== null) {
  438. break;
  439. }
  440. }
  441. $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType;
  442. }
  443. }
  444. return $this->rangeType;
  445. }
  446. /**
  447. * Create an IPv4 representation of this address (if possible, otherwise returns null).
  448. *
  449. * @return \IPLib\Address\IPv4|null
  450. */
  451. public function toIPv4()
  452. {
  453. if (strpos($this->longAddress, '2002:') === 0) {
  454. // 6to4
  455. return IPv4::fromBytes(array_slice($this->getBytes(), 2, 4));
  456. }
  457. if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) {
  458. // IPv4-mapped IPv6 addresses
  459. return IPv4::fromBytes(array_slice($this->getBytes(), -4));
  460. }
  461. return null;
  462. }
  463. /**
  464. * Render this IPv6 address in the "mixed" IPv6 (first 12 bytes) + IPv4 (last 4 bytes) mixed syntax.
  465. *
  466. * @param bool $ipV6Long render the IPv6 part in "long" format?
  467. * @param bool $ipV4Long render the IPv4 part in "long" format?
  468. *
  469. * @return string
  470. *
  471. * @example '::13.1.68.3'
  472. * @example '0000:0000:0000:0000:0000:0000:13.1.68.3' when $ipV6Long is true
  473. * @example '::013.001.068.003' when $ipV4Long is true
  474. * @example '0000:0000:0000:0000:0000:0000:013.001.068.003' when $ipV6Long and $ipV4Long are true
  475. *
  476. * @see https://tools.ietf.org/html/rfc4291#section-2.2 point 3.
  477. * @since 1.9.0
  478. */
  479. public function toMixedIPv6IPv4String($ipV6Long = false, $ipV4Long = false)
  480. {
  481. $myBytes = $this->getBytes();
  482. $ipv6Bytes = array_merge(array_slice($myBytes, 0, 12), array(0xff, 0xff, 0xff, 0xff));
  483. $ipv6String = static::fromBytes($ipv6Bytes)->toString($ipV6Long);
  484. $ipv4Bytes = array_slice($myBytes, 12, 4);
  485. $ipv4String = IPv4::fromBytes($ipv4Bytes)->toString($ipV4Long);
  486. return preg_replace('/((ffff:ffff)|(\d+(\.\d+){3}))$/i', $ipv4String, $ipv6String);
  487. }
  488. /**
  489. * {@inheritdoc}
  490. *
  491. * @see \IPLib\Address\AddressInterface::getComparableString()
  492. */
  493. public function getComparableString()
  494. {
  495. return $this->longAddress;
  496. }
  497. /**
  498. * {@inheritdoc}
  499. *
  500. * @see \IPLib\Address\AddressInterface::matches()
  501. */
  502. public function matches(RangeInterface $range)
  503. {
  504. return $range->contains($this);
  505. }
  506. /**
  507. * {@inheritdoc}
  508. *
  509. * @see \IPLib\Address\AddressInterface::getAddressAtOffset()
  510. */
  511. public function getAddressAtOffset($n)
  512. {
  513. if (!is_int($n)) {
  514. return null;
  515. }
  516. $boundary = 0x10000;
  517. $mod = $n;
  518. $words = $this->getWords();
  519. for ($i = count($words) - 1; $i >= 0; $i--) {
  520. $tmp = ($words[$i] + $mod) % $boundary;
  521. $mod = (int) floor(($words[$i] + $mod) / $boundary);
  522. if ($tmp < 0) {
  523. $tmp += $boundary;
  524. }
  525. $words[$i] = $tmp;
  526. }
  527. if ($mod !== 0) {
  528. return null;
  529. }
  530. return static::fromWords($words);
  531. }
  532. /**
  533. * {@inheritdoc}
  534. *
  535. * @see \IPLib\Address\AddressInterface::getNextAddress()
  536. */
  537. public function getNextAddress()
  538. {
  539. return $this->getAddressAtOffset(1);
  540. }
  541. /**
  542. * {@inheritdoc}
  543. *
  544. * @see \IPLib\Address\AddressInterface::getPreviousAddress()
  545. */
  546. public function getPreviousAddress()
  547. {
  548. return $this->getAddressAtOffset(-1);
  549. }
  550. /**
  551. * {@inheritdoc}
  552. *
  553. * @see \IPLib\Address\AddressInterface::getReverseDNSLookupName()
  554. */
  555. public function getReverseDNSLookupName()
  556. {
  557. return implode(
  558. '.',
  559. array_reverse(str_split(str_replace(':', '', $this->toString(true)), 1))
  560. ) . '.ip6.arpa';
  561. }
  562. }