2626 MockPortMapper = ref object of PortMapper
2727 extIp: IpAddress
2828 extPortQueue: seq [Port ]
29+ # # external ports handed out in order across successive map() calls; once
30+ # # exhausted, map() echoes the requested port. Models an IGD assigning (or
31+ # # reassigning) a different external port than requested.
2932 extPortIdx: int
3033 mapErr: Opt [string ]
3134 calls: seq [MockCall ]
@@ -37,6 +40,10 @@ proc newMock(
3740): MockPortMapper =
3841 MockPortMapper (extIp: extIp, mapErr: mapErr, extPortQueue: extPorts)
3942
43+ proc mapperFactory (m: MockPortMapper ): PortMapperFactory =
44+ proc (mode: PortMappingMode ): Opt [PortMapper ] {.gcsafe , raises : [].} =
45+ Opt .some (PortMapper (m))
46+
4047method map * (
4148 self: MockPortMapper , internalPort: Port , externalPort: Port , proto: MapProto
4249): Future [Result [MappedPort , string ]] {.async : (raises: [CancelledError ]), gcsafe .} =
@@ -65,15 +72,23 @@ method unmap*(
6572method close * (self: MockPortMapper ) {.async : (raises: []), gcsafe .} =
6673 self.calls.add (MockCall (kind: mckClose))
6774
75+ proc callsOfKind (m: MockPortMapper , kind: MockCallKind ): seq [MockCall ] =
76+ m.calls.filterIt (it.kind == kind)
77+
6878proc countCalls (m: MockPortMapper , kind: MockCallKind ): int =
69- for c in m.calls:
70- if c.kind == kind:
71- result .inc
79+ m.callsOfKind (kind).len
80+
81+ proc mapCalls (m: MockPortMapper ): seq [MockCall ] =
82+ m.callsOfKind (mckMap)
83+
84+ proc unmapCalls (m: MockPortMapper ): seq [MockCall ] =
85+ m.callsOfKind (mckUnmap)
7286
7387proc unmappedPorts (m: MockPortMapper ): seq [Port ] =
74- for c in m.calls:
75- if c.kind == mckUnmap:
76- result .add (c.externalPort)
88+ m.unmapCalls.mapIt (it.externalPort)
89+
90+ proc closed (m: MockPortMapper ): bool =
91+ m.countCalls (mckClose) > 0
7792
7893proc standardBuilder (listenAddrs: seq [MultiAddress ]): SwitchBuilder =
7994 SwitchBuilder
@@ -162,10 +177,7 @@ suite "NATService":
162177
163178 asyncTest " Upnp maps private listen addrs to extIp/extPort" :
164179 let mock = newMock ()
165- let factory: PortMapperFactory = proc (
166- mode: PortMappingMode
167- ): Opt [PortMapper ] {.gcsafe , raises : [].} =
168- Opt .some (PortMapper (mock))
180+ let factory = mapperFactory (mock)
169181
170182 let switch = makeSwitch (upnpConfig (), @ [TcpAutoAddress ], factory)
171183 await switch.start ()
@@ -184,10 +196,7 @@ suite "NATService":
184196
185197 asyncTest " Upnp preserves already-public listenAddrs alongside mapped ones" :
186198 let mock = newMock (extPorts = @ [Port (9000 )])
187- let factory: PortMapperFactory = proc (
188- mode: PortMappingMode
189- ): Opt [PortMapper ] {.gcsafe , raises : [].} =
190- Opt .some (PortMapper (mock))
199+ let factory = mapperFactory (mock)
191200
192201 let switch = makeSwitch (upnpConfig (), @ [TcpAutoAddress ], factory)
193202 await switch.start ()
@@ -204,10 +213,7 @@ suite "NATService":
204213
205214 asyncTest " Upnp unmaps stale extPort when IGD reassigns on refresh" :
206215 let mock = newMock (extPorts = @ [Port (9000 ), Port (9001 )])
207- let factory: PortMapperFactory = proc (
208- mode: PortMappingMode
209- ): Opt [PortMapper ] {.gcsafe , raises : [].} =
210- Opt .some (PortMapper (mock))
216+ let factory = mapperFactory (mock)
211217
212218 let switch = makeSwitch (upnpConfig (), @ [TcpAutoAddress ], factory)
213219 await switch.start ()
@@ -226,10 +232,7 @@ suite "NATService":
226232
227233 asyncTest " Upnp unmaps everything when private listenAddrs disappear" :
228234 let mock = newMock (extPorts = @ [Port (7000 )])
229- let factory: PortMapperFactory = proc (
230- mode: PortMappingMode
231- ): Opt [PortMapper ] {.gcsafe , raises : [].} =
232- Opt .some (PortMapper (mock))
235+ let factory = mapperFactory (mock)
233236
234237 let switch = makeSwitch (upnpConfig (), @ [TcpAutoAddress ], factory)
235238 await switch.start ()
@@ -258,10 +261,7 @@ suite "NATService":
258261
259262 asyncTest " stop unmaps active mappings and closes the mapper" :
260263 let mock = newMock (extPorts = @ [Port (5555 )])
261- let factory: PortMapperFactory = proc (
262- mode: PortMappingMode
263- ): Opt [PortMapper ] {.gcsafe , raises : [].} =
264- Opt .some (PortMapper (mock))
264+ let factory = mapperFactory (mock)
265265
266266 let switch = makeSwitch (upnpConfig (), @ [TcpAutoAddress ], factory)
267267 await switch.start ()
@@ -299,10 +299,7 @@ suite "NATService":
299299
300300 asyncTest " map failure leaves no stale entry; announced falls through" :
301301 let mock = newMock (extPorts = @ [Port (8000 )], mapErr = Opt .some (" mapping refused" ))
302- let factory: PortMapperFactory = proc (
303- mode: PortMappingMode
304- ): Opt [PortMapper ] {.gcsafe , raises : [].} =
305- Opt .some (PortMapper (mock))
302+ let factory = mapperFactory (mock)
306303
307304 let switch = makeSwitch (upnpConfig (), @ [TcpAutoAddress ], factory)
308305 await switch.start ()
@@ -320,10 +317,7 @@ suite "NATService":
320317 # libplum maps IPv4 only, so any IPv6 listenAddr (even ULA fc00::/7) must be
321318 # filtered out before map().
322319 let mock = newMock ()
323- let factory: PortMapperFactory = proc (
324- mode: PortMappingMode
325- ): Opt [PortMapper ] {.gcsafe , raises : [].} =
326- Opt .some (PortMapper (mock))
320+ let factory = mapperFactory (mock)
327321
328322 let switch = makeSwitch (upnpConfig (), @ [TcpAutoAddress ], factory)
329323 await switch.start ()
@@ -385,10 +379,7 @@ suite "NATService":
385379 # NATConfig keeps mode (port-mapping) and autonat orthogonal: enabling
386380 # both must spin up the UPnP addressMapper *and* the AutonatService.
387381 let mock = newMock ()
388- let factory: PortMapperFactory = proc (
389- mode: PortMappingMode
390- ): Opt [PortMapper ] {.gcsafe , raises : [].} =
391- Opt .some (PortMapper (mock))
382+ let factory = mapperFactory (mock)
392383
393384 var cfg = upnpConfig ()
394385 cfg.reachability = autonatConfig (AutonatV1 ).reachability
@@ -457,10 +448,7 @@ suite "NATService":
457448
458449 asyncTest " withNAT can be called once per distinct concern" :
459450 let mock = newMock ()
460- let factory: PortMapperFactory = proc (
461- mode: PortMappingMode
462- ): Opt [PortMapper ] {.gcsafe , raises : [].} =
463- Opt .some (PortMapper (mock))
451+ let factory = mapperFactory (mock)
464452
465453 let switch = standardBuilder (@ [TcpAutoAddress ])
466454 .withNAT (upnpConfig (), factory)
@@ -482,50 +470,6 @@ suite "NATService":
482470 .withNAT (autonatConfig (AutonatV1 ))
483471 .withNAT (autonatConfig (AutonatV2 ))
484472
485- type RecordingPortMapper = ref object of PortMapper
486- externalIp: IpAddress
487- mapErr: Opt [string ]
488- mapPortOverride: Opt [Port ]
489- # # When set, `map` returns this port instead of echoing the requested
490- # # `externalPort`. Models an IGD that re-maps to a different external port
491- # # (e.g. when the requested one is busy).
492- unmapResult: Result [void , string ]
493- mapCalls: seq [tuple [internal, external: Port , proto: MapProto ]]
494- unmapCalls: seq [tuple [external: Port , proto: MapProto ]]
495- closed: bool
496-
497- method map * (
498- self: RecordingPortMapper , internalPort: Port , externalPort: Port , proto: MapProto
499- ): Future [Result [MappedPort , string ]] {.async : (raises: [CancelledError ]), gcsafe .} =
500- self.mapCalls.add ((internalPort, externalPort, proto))
501- self.mapErr.withValue (e):
502- return err (e)
503- let ext = self.mapPortOverride.get (externalPort)
504- ok (MappedPort (externalIp: self.externalIp, externalPort: ext))
505-
506- method unmap * (
507- self: RecordingPortMapper , externalPort: Port , proto: MapProto
508- ): Future [Result [void , string ]] {.async : (raises: [CancelledError ]), gcsafe .} =
509- self.unmapCalls.add ((externalPort, proto))
510- self.unmapResult
511-
512- method close * (self: RecordingPortMapper ) {.async : (raises: []), gcsafe .} =
513- self.closed = true
514-
515- proc newRecordingOk (externalIp: IpAddress ): RecordingPortMapper =
516- RecordingPortMapper (externalIp: externalIp, unmapResult: Result [void , string ].ok ())
517-
518- proc recordingFactory (m: RecordingPortMapper ): PortMapperFactory =
519- return proc (mode: PortMappingMode ): Opt [PortMapper ] {.gcsafe , raises : [].} =
520- Opt .some (PortMapper (m))
521-
522- proc recordingFactoryFail (): PortMapperFactory =
523- recordingFactory (
524- RecordingPortMapper (
525- mapErr: Opt .some (" mock no IGD" ), unmapResult: Result [void , string ].ok ()
526- )
527- )
528-
529473proc loopbackAddr (): MultiAddress =
530474 MultiAddress .init (" /ip4/127.0.0.1/tcp/0" ).get ()
531475
@@ -539,8 +483,8 @@ suite "NATService (setupMappings)":
539483 asyncTest " NatPmp announces external IP after successful mapping" :
540484 let
541485 externalIp = parseIpAddress (" 203.0.113.99" )
542- mapper = newRecordingOk ( externalIp)
543- factory = recordingFactory (mapper)
486+ mapper = newMock (extIp = externalIp)
487+ factory = mapperFactory (mapper)
544488 cfg = natPmpConfig ()
545489 switch = makeSwitch (cfg, @ [loopbackAddr ()], factory)
546490 svc = findNatService (switch)
@@ -559,8 +503,8 @@ suite "NATService (setupMappings)":
559503 # on a v4 listen address, so the mapping produces no announced address
560504 let
561505 externalIp = parseIpAddress (" 2001:db8::1" )
562- mapper = newRecordingOk ( externalIp)
563- factory = recordingFactory (mapper)
506+ mapper = newMock (extIp = externalIp)
507+ factory = mapperFactory (mapper)
564508 cfg = upnpConfig ()
565509 switch = makeSwitch (cfg, @ [loopbackAddr ()], factory)
566510 svc = findNatService (switch)
@@ -576,8 +520,8 @@ suite "NATService (setupMappings)":
576520 asyncTest " non-private listen addresses are skipped" :
577521 let
578522 externalIp = parseIpAddress (" 203.0.113.1" )
579- mapper = newRecordingOk ( externalIp)
580- factory = recordingFactory (mapper)
523+ mapper = newMock (extIp = externalIp)
524+ factory = mapperFactory (mapper)
581525 cfg = upnpConfig ()
582526 switch = makeSwitch (cfg, @ [loopbackAddr ()], factory)
583527 svc = findNatService (switch)
@@ -601,8 +545,8 @@ suite "NATService (setupMappings)":
601545 let
602546 externalIp = parseIpAddress (" 203.0.113.111" )
603547 userAddr = MultiAddress .init (" /ip4/198.51.100.7/tcp/4242" ).tryGet ()
604- mapper = newRecordingOk ( externalIp)
605- factory = recordingFactory (mapper)
548+ mapper = newMock (extIp = externalIp)
549+ factory = mapperFactory (mapper)
606550 cfg = upnpConfig ()
607551 switch = makeSwitch (cfg, @ [loopbackAddr ()], factory)
608552
@@ -620,8 +564,8 @@ suite "NATService (setupMappings)":
620564 asyncTest " multiple private listen addresses are each mapped once" :
621565 let
622566 externalIp = parseIpAddress (" 203.0.113.10" )
623- mapper = newRecordingOk ( externalIp)
624- factory = recordingFactory (mapper)
567+ mapper = newMock (extIp = externalIp)
568+ factory = mapperFactory (mapper)
625569 cfg = upnpConfig ()
626570 switch = makeSwitch (cfg, @ [loopbackAddr ()], factory)
627571 svc = findNatService (switch)
@@ -647,8 +591,8 @@ suite "NATService (setupMappings)":
647591 asyncTest " setupMappings unmaps stale ports when a listen addr is removed" :
648592 let
649593 externalIp = parseIpAddress (" 203.0.113.20" )
650- mapper = newRecordingOk ( externalIp)
651- factory = recordingFactory (mapper)
594+ mapper = newMock (extIp = externalIp)
595+ factory = mapperFactory (mapper)
652596 cfg = upnpConfig ()
653597 switch = makeSwitch (cfg, @ [loopbackAddr ()], factory)
654598 svc = findNatService (switch)
@@ -665,22 +609,20 @@ suite "NATService (setupMappings)":
665609 # Second cycle: one of them is gone. unmapStale must clean it up.
666610 discard await svc.setupMappings (@ [privateAddr (9000 )])
667611 check mapper.unmapCalls.len == 1
668- check mapper.unmapCalls[^ 1 ].external == Port (9001 )
612+ check mapper.unmapCalls[^ 1 ].externalPort == Port (9001 )
669613 check mapper.unmapCalls[^ 1 ].proto == mpTcp
670614
671615 asyncTest " IGD returning a different external port surfaces in announced" :
672616 let
673617 externalIp = parseIpAddress (" 203.0.113.30" )
674- mapper = newRecordingOk (externalIp)
675- factory = recordingFactory (mapper)
618+ # queued external port simulates the IGD remapping the request to a
619+ # different port (e.g. because the requested one is already busy).
620+ mapper = newMock (extIp = externalIp, extPorts = @ [Port (54321 )])
621+ factory = mapperFactory (mapper)
676622 cfg = upnpConfig ()
677623 switch = makeSwitch (cfg, @ [loopbackAddr ()], factory)
678624 svc = findNatService (switch)
679625
680- # Simulate the IGD remapping the request to a different external port
681- # (e.g. because the requested one is already busy on the gateway).
682- mapper.mapPortOverride = Opt .some (Port (54321 ))
683-
684626 await switch.start ()
685627 defer :
686628 await switch.stop ()
@@ -694,7 +636,8 @@ suite "NATService (setupMappings)":
694636 asyncTest " NatPmp mapping failure leaves announced empty" :
695637 let
696638 cfg = natPmpConfig ()
697- switch = makeSwitch (cfg, @ [loopbackAddr ()], recordingFactoryFail ())
639+ mapper = newMock (mapErr = Opt .some (" mock no IGD" ))
640+ switch = makeSwitch (cfg, @ [loopbackAddr ()], mapperFactory (mapper))
698641 svc = findNatService (switch)
699642
700643 await switch.start ()
@@ -708,8 +651,8 @@ suite "NATService (setupMappings)":
708651 asyncTest " NatPmp stop unmaps all created mappings" :
709652 let
710653 externalIp = parseIpAddress (" 203.0.113.40" )
711- mapper = newRecordingOk ( externalIp)
712- factory = recordingFactory (mapper)
654+ mapper = newMock (extIp = externalIp)
655+ factory = mapperFactory (mapper)
713656 cfg = natPmpConfig ()
714657 switch = makeSwitch (cfg, @ [loopbackAddr ()], factory)
715658 svc = findNatService (switch)
0 commit comments