A Discrete-Event Network Simulator
API
Loading...
Searching...
No Matches
multi-link-element.cc
Go to the documentation of this file.
1/*
2 * Copyright (c) 2021 Universita' degli Studi di Napoli Federico II
3 *
4 * SPDX-License-Identifier: GPL-2.0-only
5 *
6 * Author: Stefano Avallone <stavallo@unina.it>
7 */
8
10
11#include "ns3/address-utils.h"
12#include "ns3/log.h"
13#include "ns3/mgt-headers.h"
14
15#include <utility>
16
17NS_LOG_COMPONENT_DEFINE("MultiLinkElement");
18
19namespace ns3
20{
21
23 : m_containingFrame(frame),
24 m_commonInfo(std::in_place_type<std::monostate>) // initialize as UNSET
25{
26}
27
29 : MultiLinkElement(frame)
30{
31 NS_ASSERT(variant != UNSET);
32 SetVariant(variant);
33}
34
37{
38 return IE_EXTENSION;
39}
40
46
49{
50 return static_cast<Variant>(m_commonInfo.index());
51}
52
53void
55{
56 NS_ABORT_MSG_IF(GetVariant() != UNSET, "Multi-Link Element variant already set");
57 NS_ABORT_MSG_IF(variant == UNSET, "Invalid variant");
58
59 switch (variant)
60 {
61 case BASIC_VARIANT:
63 break;
66 break;
67 default:
68 NS_ABORT_MSG("Unsupported variant: " << +variant);
69 }
70}
71
74{
75 return std::get<BASIC_VARIANT>(m_commonInfo);
76}
77
80{
81 return std::get<BASIC_VARIANT>(m_commonInfo);
82}
83
84void
86{
87 std::get<BASIC_VARIANT>(m_commonInfo).m_mldMacAddress = address;
88}
89
92{
93 return std::get<BASIC_VARIANT>(m_commonInfo).m_mldMacAddress;
94}
95
96void
98{
99 std::get<BASIC_VARIANT>(m_commonInfo).m_linkIdInfo = (linkIdInfo & 0x0f);
100}
101
102bool
104{
105 return std::get<BASIC_VARIANT>(m_commonInfo).m_linkIdInfo.has_value();
106}
107
108uint8_t
110{
111 return std::get<BASIC_VARIANT>(m_commonInfo).m_linkIdInfo.value();
112}
113
114void
116{
117 std::get<BASIC_VARIANT>(m_commonInfo).m_bssParamsChangeCount = count;
118}
119
120bool
122{
123 return std::get<BASIC_VARIANT>(m_commonInfo).m_bssParamsChangeCount.has_value();
124}
125
126uint8_t
128{
129 return std::get<BASIC_VARIANT>(m_commonInfo).m_bssParamsChangeCount.value();
130}
131
132void
134{
135 auto& emlCapabilities = std::get<BASIC_VARIANT>(m_commonInfo).m_emlCapabilities;
136 if (!emlCapabilities.has_value())
137 {
138 emlCapabilities = CommonInfoBasicMle::EmlCapabilities{};
139 }
140 emlCapabilities->emlsrSupport = supported ? 1 : 0;
141}
142
143void
145{
146 auto& emlCapabilities = std::get<BASIC_VARIANT>(m_commonInfo).m_emlCapabilities;
147 if (!emlCapabilities.has_value())
148 {
149 emlCapabilities = CommonInfoBasicMle::EmlCapabilities{};
150 }
152}
153
154void
156{
157 auto& emlCapabilities = std::get<BASIC_VARIANT>(m_commonInfo).m_emlCapabilities;
158 if (!emlCapabilities.has_value())
159 {
160 emlCapabilities = CommonInfoBasicMle::EmlCapabilities{};
161 }
163}
164
165void
167{
168 auto& emlCapabilities = std::get<BASIC_VARIANT>(m_commonInfo).m_emlCapabilities;
169 if (!emlCapabilities.has_value())
170 {
171 emlCapabilities = CommonInfoBasicMle::EmlCapabilities{};
172 }
173 auto timeoutUs = timeout.GetMicroSeconds();
174
175 if (timeoutUs == 0)
176 {
177 emlCapabilities->transitionTimeout = 0;
178 }
179 else
180 {
181 uint8_t i;
182 for (i = 1; i <= 10; i++)
183 {
184 if (1 << (i + 6) == timeoutUs)
185 {
186 emlCapabilities->transitionTimeout = i;
187 break;
188 }
189 }
190 NS_ABORT_MSG_IF(i > 10, "Value not allowed (" << timeout.As(Time::US) << ")");
191 }
192}
193
194bool
196{
197 return std::get<BASIC_VARIANT>(m_commonInfo).m_emlCapabilities.has_value();
198}
199
200bool
202{
203 return std::get<BASIC_VARIANT>(m_commonInfo).m_emlCapabilities->emlsrSupport;
204}
205
206Time
208{
209 auto& emlCapabilities = std::get<BASIC_VARIANT>(m_commonInfo).m_emlCapabilities;
210 NS_ASSERT(emlCapabilities);
211 return CommonInfoBasicMle::DecodeEmlsrPaddingDelay(emlCapabilities->emlsrPaddingDelay);
212}
213
214Time
216{
217 auto& emlCapabilities = std::get<BASIC_VARIANT>(m_commonInfo).m_emlCapabilities;
218 NS_ASSERT(emlCapabilities);
219 return CommonInfoBasicMle::DecodeEmlsrTransitionDelay(emlCapabilities->emlsrTransitionDelay);
220}
221
222Time
224{
225 auto& emlCapabilities = std::get<BASIC_VARIANT>(m_commonInfo).m_emlCapabilities;
226 NS_ASSERT(emlCapabilities);
227 if (emlCapabilities->transitionTimeout == 0)
228 {
229 return MicroSeconds(0);
230 }
231 return MicroSeconds(1 << (6 + emlCapabilities->transitionTimeout));
232}
233
234void
236{
237 const auto variant = GetVariant();
238 switch (variant)
239 {
240 case BASIC_VARIANT:
241 std::get<CommonInfoBasicMle>(m_commonInfo).m_apMldId = id;
242 return;
244 std::get<CommonInfoProbeReqMle>(m_commonInfo).m_apMldId = id;
245 return;
246 default:
247 NS_ABORT_MSG("AP MLD ID field not present in input variant " << variant);
248 }
249}
250
251std::optional<uint8_t>
253{
254 const auto variant = GetVariant();
255 switch (variant)
256 {
257 case BASIC_VARIANT:
258 return std::get<CommonInfoBasicMle>(m_commonInfo).m_apMldId;
260 return std::get<CommonInfoProbeReqMle>(m_commonInfo).m_apMldId;
261 default:
262 NS_LOG_DEBUG("AP MLD ID field not present in input variant");
263 }
264 return std::nullopt;
265}
266
268 : m_variant(variant),
269 m_staControl(0)
270{
271}
272
274 const PerStaProfileSubelement& perStaProfile)
275 : m_variant(perStaProfile.m_variant),
276 m_staControl(perStaProfile.m_staControl),
277 m_staMacAddress(perStaProfile.m_staMacAddress),
278 m_bssParamsChgCnt(perStaProfile.m_bssParamsChgCnt)
279{
280 // deep copy of the STA Profile field
281 auto staProfileCopy = [&](auto&& frame) {
282 using Ptr = std::decay_t<decltype(frame)>;
283 if constexpr (std::is_same_v<Ptr, std::monostate>)
284 {
285 return;
286 }
287 else
288 {
289 using T = std::decay_t<decltype(*frame.get())>;
290 m_staProfile = std::make_unique<T>(*frame.get());
291 }
292 };
293 std::visit(staProfileCopy, perStaProfile.m_staProfile);
294}
295
298{
299 // check for self-assignment
300 if (&perStaProfile == this)
301 {
302 return *this;
303 }
304
305 m_variant = perStaProfile.m_variant;
306 m_staControl = perStaProfile.m_staControl;
307 m_staMacAddress = perStaProfile.m_staMacAddress;
308 m_bssParamsChgCnt = perStaProfile.m_bssParamsChgCnt;
309
310 // deep copy of the STA Profile field
311 auto staProfileCopy = [&](auto&& frame) {
312 using Ptr = std::decay_t<decltype(frame)>;
313 if constexpr (std::is_same_v<Ptr, std::monostate>)
314 {
315 return;
316 }
317 else
318 {
319 using T = std::decay_t<decltype(*frame.get())>;
320 m_staProfile = std::make_unique<T>(*frame.get());
321 }
322 };
323 std::visit(staProfileCopy, perStaProfile.m_staProfile);
324
325 return *this;
326}
327
328void
330{
331 m_staControl &= 0xfff0; // reset Link ID subfield in the STA Control field
332 m_staControl |= (linkId & 0x0f);
333}
334
335uint8_t
337{
338 return static_cast<uint8_t>(m_staControl & 0x000f);
339}
340
341void
346
347bool
349{
350 return (m_staControl & 0x0010) != 0;
351}
352
353void
355{
356 NS_ABORT_IF(m_variant != BASIC_VARIANT);
357 m_staMacAddress = address;
358 m_staControl |= 0x0020;
359}
360
361bool
363{
364 return (m_staControl & 0x0020) != 0;
365}
366
369{
370 NS_ABORT_IF(!HasStaMacAddress());
371 return m_staMacAddress;
372}
373
374void
376{
377 NS_ABORT_MSG_IF(m_variant != BASIC_VARIANT,
378 "Expected Basic Variant, variant:" << +static_cast<uint8_t>(m_variant));
379 m_bssParamsChgCnt = count;
380 m_staControl |= 0x0800;
381}
382
383bool
385{
386 return (m_staControl & 0x0800) != 0;
387}
388
389uint8_t
391{
392 NS_ASSERT_MSG(m_bssParamsChgCnt.has_value(), "No value set for m_bssParamsChgCnt");
393 NS_ASSERT_MSG(HasBssParamsChgCnt(), "BSS Parameters Change count bit not set");
394 return m_bssParamsChgCnt.value();
395}
396
397void
399 const std::variant<MgtAssocRequestHeader, MgtReassocRequestHeader>& assoc)
400{
401 std::visit(
402 [&](auto&& frame) {
403 m_staProfile = std::make_unique<std::decay_t<decltype(frame)>>(frame);
404 },
405 assoc);
406}
407
408void
410 std::variant<MgtAssocRequestHeader, MgtReassocRequestHeader>&& assoc)
411{
412 std::visit(
413 [&](auto&& frame) {
414 using T = std::decay_t<decltype(frame)>;
415 m_staProfile = std::make_unique<T>(std::forward<T>(frame));
416 },
417 assoc);
418}
419
420bool
422{
423 return std::holds_alternative<std::unique_ptr<MgtAssocRequestHeader>>(m_staProfile);
424}
425
426bool
428{
429 return std::holds_alternative<std::unique_ptr<MgtReassocRequestHeader>>(m_staProfile);
430}
431
434{
435 if (HasAssocRequest())
436 {
437 return *std::get<std::unique_ptr<MgtAssocRequestHeader>>(m_staProfile);
438 }
439 NS_ABORT_UNLESS(HasReassocRequest());
440 return *std::get<std::unique_ptr<MgtReassocRequestHeader>>(m_staProfile);
441}
442
443void
445{
446 m_staProfile = std::make_unique<MgtAssocResponseHeader>(assoc);
447}
448
449void
451{
452 m_staProfile = std::make_unique<MgtAssocResponseHeader>(std::move(assoc));
453}
454
455bool
457{
458 return std::holds_alternative<std::unique_ptr<MgtAssocResponseHeader>>(m_staProfile);
459}
460
463{
464 NS_ABORT_IF(!HasAssocResponse());
465 return *std::get<std::unique_ptr<MgtAssocResponseHeader>>(m_staProfile);
466}
467
468void
470{
471 m_staProfile = std::make_unique<MgtProbeResponseHeader>(probeResp);
472}
473
474void
476{
477 m_staProfile = std::make_unique<MgtProbeResponseHeader>(std::move(probeResp));
478}
479
480bool
482{
483 return std::holds_alternative<std::unique_ptr<MgtProbeResponseHeader>>(m_staProfile);
484}
485
488{
489 NS_ABORT_IF(!HasProbeResponse());
490 return *std::get<std::unique_ptr<MgtProbeResponseHeader>>(m_staProfile);
491}
492
493uint8_t
495{
496 if (m_variant == PROBE_REQUEST_VARIANT)
497 {
498 return 0; // IEEE 802.11be 6.0 Figure 9-1072s
499 }
500
501 uint8_t ret = 1; // STA Info Length
502
503 if (HasStaMacAddress())
504 {
505 ret += 6;
506 }
507 if (HasBssParamsChgCnt())
508 {
509 ret += 1;
510 }
511 // TODO add other subfields of the STA Info field
512 return ret;
513}
514
520
521uint16_t
523{
524 uint16_t ret = 2; // STA Control field
525
526 ret += GetStaInfoLength();
527
528 auto staProfileSize = [&](auto&& frame) {
529 using T = std::decay_t<decltype(frame)>;
530 if constexpr (std::is_same_v<T, std::monostate>)
531 {
532 NS_ASSERT_MSG(std::holds_alternative<std::monostate>(m_containingFrame),
533 "Missing management frame for Per-STA Profile subelement");
534 return static_cast<uint32_t>(0);
535 }
536 else
537 {
538 using U = std::decay_t<decltype(*frame)>;
540 std::holds_alternative<std::reference_wrapper<const U>>(m_containingFrame),
541 "Containing frame type and frame type in Per-STA Profile do not match");
542 const auto& containing = std::get<std::reference_wrapper<const U>>(m_containingFrame);
543 return frame->GetSerializedSizeInPerStaProfile(containing);
544 }
545 };
546 ret += std::visit(staProfileSize, m_staProfile);
547
548 return ret;
549}
550
551void
553{
554 if (m_variant == PROBE_REQUEST_VARIANT)
555 {
556 NS_ASSERT_MSG(IsCompleteProfileSet(), "Encoding of STA Profile not supported");
557 start.WriteHtolsbU16(m_staControl);
558 return;
559 }
560
561 start.WriteHtolsbU16(m_staControl);
562 start.WriteU8(GetStaInfoLength());
563
564 if (HasStaMacAddress())
565 {
566 WriteTo(start, m_staMacAddress);
567 }
568 if (HasBssParamsChgCnt())
569 {
570 start.WriteU8(GetBssParamsChgCnt());
571 }
572 // TODO add other subfields of the STA Info field
573 auto staProfileSerialize = [&](auto&& frame) {
574 using T = std::decay_t<decltype(frame)>;
575 if constexpr (std::is_same_v<T, std::monostate>)
576 {
577 NS_ASSERT_MSG(std::holds_alternative<std::monostate>(m_containingFrame),
578 "Missing management frame for Per-STA Profile subelement");
579 return;
580 }
581 else
582 {
583 using U = std::decay_t<decltype(*frame)>;
585 std::holds_alternative<std::reference_wrapper<const U>>(m_containingFrame),
586 "Containing frame type and frame type in Per-STA Profile do not match");
587 const auto& containing = std::get<std::reference_wrapper<const U>>(m_containingFrame);
588 frame->SerializeInPerStaProfile(start, containing);
589 }
590 };
591 std::visit(staProfileSerialize, m_staProfile);
592}
593
594uint16_t
596 uint16_t length)
597{
598 if (m_variant == PROBE_REQUEST_VARIANT)
599 {
600 return DeserProbeReqMlePerSta(start, length);
601 }
602
603 Buffer::Iterator i = start;
604
605 m_staControl = i.ReadLsbtohU16();
606 i.ReadU8(); // STA Info Length
607
608 if (HasStaMacAddress())
609 {
610 ReadFrom(i, m_staMacAddress);
611 }
612 if (HasBssParamsChgCnt())
613 {
614 m_bssParamsChgCnt = i.ReadU8();
615 }
616
617 // TODO add other subfields of the STA Info field
618 uint16_t count = i.GetDistanceFrom(start);
619
620 NS_ASSERT_MSG(count <= length,
621 "Bytes read (" << count << ") exceed expected number (" << length << ")");
622
623 if (count == length)
624 {
625 return count;
626 }
627
628 auto staProfileDeserialize = [&](auto&& frame) {
629 using T = std::decay_t<decltype(frame)>;
630 if constexpr (!std::is_same_v<T, std::monostate>)
631 {
632 using U = std::decay_t<decltype(frame.get())>;
633 U assoc;
634 count += assoc.DeserializeFromPerStaProfile(i, length - count, frame.get());
635 m_staProfile = std::make_unique<U>(std::move(assoc));
636 }
637 };
638 std::visit(staProfileDeserialize, m_containingFrame);
639
640 return count;
641}
642
643uint16_t
645 uint16_t length)
646{
648 "Invalid Multi-link Element variant = " << static_cast<uint8_t>(m_variant));
649 Buffer::Iterator i = start;
650 uint16_t count = 0;
651
652 m_staControl = i.ReadLsbtohU16();
653 count += 2;
654
655 NS_ASSERT_MSG(count <= length,
656 "Incorrect decoded size count =" << count << ", length=" << length);
657 if (count == length)
658 {
659 return count;
660 }
661
662 // TODO: Support decoding of Partial Per-STA Profile
663 // IEEE 802.11be D5.0 9.4.2.312.3 Probe Request Multi-Link element
664 // If the Complete Profile Requested subfield is set to 0 and the STA Profile field
665 // is present in a Per-STA Profile subelement,
666 // the STA Profile field includes exactly one of the following:
667 // - one Request element (see 9.4.2.9 (Request element)), or
668 // — one Extended Request element (see 9.4.2.10 (Extended Request element)), or
669 // — one Request element and one Extended Request element
670 NS_LOG_DEBUG("Decoding of STA Profile in Per-STA Profile subelement not supported");
671 while (count < length)
672 {
673 i.ReadU8();
674 count++;
675 }
676 return count;
677}
678
679void
681{
682 os << "Per-STA Profile Subelement=[";
683 os << "Variant: " << +static_cast<uint8_t>(m_variant);
684 os << ", STA Control: " << +m_staControl;
685 if (HasStaMacAddress())
686 {
687 os << ", STA MAC Address: " << m_staMacAddress;
688 }
689 if (m_bssParamsChgCnt)
690 {
691 os << ", BSS Params Change Count: " << +m_bssParamsChgCnt.value();
692 }
693 os << "]";
694}
695
696void
698{
699 auto variant = GetVariant();
700 NS_ABORT_IF(variant == UNSET);
701 m_perStaProfileSubelements.emplace_back(variant);
702}
703
704std::size_t
709
712{
713 return m_perStaProfileSubelements.at(i);
714}
715
718{
719 return m_perStaProfileSubelements.at(i);
720}
721
722uint16_t
724{
725 uint16_t ret = 3; // ElementIdExt (1) + Multi-Link Control (2)
726
727 // add the Common Info field size (dependent on the Multi-Link Element variant)
728 ret += std::visit(
729 [](auto&& arg) -> uint8_t {
730 using T = std::decay_t<decltype(arg)>;
731 if constexpr (std::is_same_v<T, std::monostate>)
732 {
733 NS_ABORT_MSG("Multi-Link Element variant not set");
734 return 0;
735 }
736 else
737 {
738 return arg.GetSize();
739 }
740 },
742
743 for (const auto& subelement : m_perStaProfileSubelements)
744 {
745 subelement.m_containingFrame = m_containingFrame;
746 ret += subelement.GetSerializedSize();
747 }
748
749 return ret;
750}
751
752void
754{
755 // serialize the Multi-Link Control and Common Info fields
756 std::visit(
757 [this, &start](auto&& arg) {
758 using T = std::decay_t<decltype(arg)>;
759 if constexpr (std::is_same_v<T, std::monostate>)
760 {
761 NS_ABORT_MSG("Multi-Link Element variant not set");
762 }
763 else
764 {
765 uint16_t mlControl =
766 static_cast<uint8_t>(GetVariant()) + (arg.GetPresenceBitmap() << 4);
767 start.WriteHtolsbU16(mlControl);
768 arg.Serialize(start);
769 }
770 },
772
773 for (const auto& subelement : m_perStaProfileSubelements)
774 {
775 start = subelement.Serialize(start);
776 }
777}
778
779uint16_t
781{
782 Buffer::Iterator i = start;
783 uint16_t count = 0;
784
785 uint16_t mlControl = i.ReadLsbtohU16();
786 count += 2;
787
788 SetVariant(static_cast<Variant>(mlControl & 0x0007));
789 uint16_t presence = mlControl >> 4;
790
791 uint8_t nBytes = std::visit(
792 [&i, &presence](auto&& arg) -> uint8_t {
793 using T = std::decay_t<decltype(arg)>;
794 if constexpr (std::is_same_v<T, std::monostate>)
795 {
796 NS_ABORT_MSG("Multi-Link Element variant not set");
797 return 0;
798 }
799 else
800 {
801 return arg.Deserialize(i, presence);
802 }
803 },
805 i.Next(nBytes);
806 count += nBytes;
807
808 while (count < length)
809 {
810 switch (static_cast<SubElementId>(i.PeekU8()))
811 {
814 auto& perStaProfile = GetPerStaProfile(GetNPerStaProfileSubelements() - 1);
815 perStaProfile.m_containingFrame = m_containingFrame;
816 i = perStaProfile.Deserialize(i);
817 count = i.GetDistanceFrom(start);
818 }
819 break;
820 default:
821 NS_ABORT_MSG("Unsupported Subelement ID: " << +i.PeekU8());
822 }
823 }
824
825 return count;
826}
827
828void
829MultiLinkElement::Print(std::ostream& os) const
830{
831 os << "Multi-Link Element=[Per-STA Profile Subelements: {";
832 for (const auto& subelement : m_perStaProfileSubelements)
833 {
834 subelement.Print(os);
835 }
836 os << "}]";
837}
838
839} // namespace ns3
iterator in a Buffer instance
Definition buffer.h:89
uint16_t ReadLsbtohU16()
Definition buffer.cc:1053
uint32_t GetDistanceFrom(const Iterator &o) const
Definition buffer.cc:769
void Next()
go forward by one byte
Definition buffer.h:842
an EUI-48 address
Implement the header for management frames of type association and reassociation response.
Implement the header for management frames of type probe response.
Smart pointer class similar to boost::intrusive_ptr.
Simulation virtual time values and global simulation resolution.
Definition nstime.h:94
TimeWithUnit As(const Unit unit=Time::AUTO) const
Attach a unit to a Time, to facilitate output in a specific unit.
Definition time.cc:403
@ US
microsecond
Definition nstime.h:107
int64_t GetMicroSeconds() const
Get an approximation of the time stored in this instance in the indicated unit.
Definition nstime.h:402
#define NS_ASSERT(condition)
At runtime, in debugging builds, if this condition is not true, the program prints the source file,...
Definition assert.h:55
#define NS_ASSERT_MSG(condition, message)
At runtime, in debugging builds, if this condition is not true, the program prints the message to out...
Definition assert.h:75
#define NS_ABORT_MSG(msg)
Unconditional abnormal program termination with a message.
Definition abort.h:38
#define NS_ABORT_UNLESS(cond)
Abnormal program termination if a condition is false.
Definition abort.h:118
#define NS_ABORT_MSG_IF(cond, msg)
Abnormal program termination if a condition is true, with a message.
Definition abort.h:97
#define NS_ABORT_IF(cond)
Abnormal program termination if a condition is true.
Definition abort.h:65
#define NS_LOG_COMPONENT_DEFINE(name)
Define a Log component with a specific name.
Definition log.h:191
#define NS_LOG_DEBUG(msg)
Use NS_LOG to output a message of level LOG_DEBUG.
Definition log.h:257
Time MicroSeconds(uint64_t value)
Construct a Time in the indicated unit.
Definition nstime.h:1369
Variant
Multi-Link element variants.
Every class exported by the ns3 library is enclosed in the ns3 namespace.
void WriteTo(Buffer::Iterator &i, Ipv4Address ad)
Write an Ipv4Address to a Buffer.
std::variant< std::reference_wrapper< MgtAssocRequestHeader >, std::reference_wrapper< MgtReassocRequestHeader > > AssocReqRefVariant
variant holding a reference to a (Re)Association Request
Definition ap-wifi-mac.h:45
uint8_t WifiInformationElementId
This type is used to represent an Information Element ID.
void ReadFrom(Buffer::Iterator &i, Ipv4Address &ad)
Read an Ipv4Address from a Buffer.
STL namespace.
ns3::Time timeout
uint8_t emlsrTransitionDelay
EMLSR Transition Delay.
Common Info field of the Basic Multi-Link element.
static uint8_t EncodeEmlsrTransitionDelay(Time delay)
static Time DecodeEmlsrTransitionDelay(uint8_t value)
static Time DecodeEmlsrPaddingDelay(uint8_t value)
static uint8_t EncodeEmlsrPaddingDelay(Time delay)
Common Info field of Multi-link Element Probe Request variant.
#define IE_EXTENSION
#define IE_EXT_MULTI_LINK_ELEMENT