@@ -296,6 +296,16 @@ func (t *NATTypeTest) TestMappingBehaviour() error {
296296 return nil
297297 }
298298
299+ // Validate OTHER-ADDRESS has both a different IP and port from the primary server.
300+ // The mapping behavior test requires a genuinely distinct endpoint to produce meaningful results.
301+ serverUDPForValidation , ok := t .TestServer .(* net.UDPAddr )
302+ if ! ok {
303+ return errors .New ("TestServer is not a UDP address" )
304+ }
305+ if otherAddr .IP .Equal (serverUDPForValidation .IP ) || otherAddr .Port == serverUDPForValidation .Port {
306+ return errors .New ("OTHER-ADDRESS from STUN server does not differ in both IP and port from the primary server" )
307+ }
308+
299309 // Test II: From same socket, binding to OTHER-ADDRESS (different IP and port)
300310 altAddr := & net.UDPAddr {IP : otherAddr .IP , Port : otherAddr .Port }
301311 resp2 , _ , err := t .doTransactionWithRetry (conn , localAddr , altAddr , t .Attempts ,
@@ -594,32 +604,61 @@ func (t *NATTypeTest) CalcReminderValues() error {
594604 t .SingleSourceIPSourceNATMapping = NATYesOrNoUnknownType_No
595605 }
596606
597- allSendToMatchRespFrom := true
598- validPairCount := 0
607+ // PreserveSourceIPPortWhenDestNATMapping: check using RESPONSE-ORIGIN if available,
608+ // otherwise fall back to CHANGE-REQUEST hairpin detection.
609+ allResponseOriginMatchRespFrom := true
610+ responseOriginPairCount := 0
599611 for _ , tc := range transcripts {
600612 if tc .Resp == nil || tc .RespFrom == nil || tc .ReqSentTo == nil {
601613 continue
602614 }
603- if value , ok := tc .Req .Attributes .Get (stun .AttrChangeRequest ); ok {
604- if len (value .Value ) != 4 || (value .Value [0 ] == 0 && value .Value [1 ] == 0 && value .Value [2 ] == 0 && value .Value [3 ] == 0 ) {
605- continue
606- }
607- } else {
615+ var responseOrigin stun.ResponseOrigin
616+ if err := responseOrigin .GetFrom (tc .Resp ); err != nil {
608617 continue
609618 }
610- validPairCount ++
611- if tc .RespFrom .String () != tc .ReqSentTo .String () {
612- allSendToMatchRespFrom = false
619+ responseOriginAddr := net.UDPAddr {IP : responseOrigin .IP , Port : responseOrigin .Port }
620+ if responseOriginAddr .String () == tc .ReqSentTo .String () {
621+ continue
622+ }
623+ responseOriginPairCount ++
624+ if tc .RespFrom .String () != responseOriginAddr .String () {
625+ allResponseOriginMatchRespFrom = false
613626 break
614627 }
615628 }
616- switch {
617- case validPairCount < 1 :
618- t .PreserveSourceIPPortWhenDestNATMapping = NATYesOrNoUnknownType_Unknown
619- case allSendToMatchRespFrom :
620- t .PreserveSourceIPPortWhenDestNATMapping = NATYesOrNoUnknownType_No
621- default :
622- t .PreserveSourceIPPortWhenDestNATMapping = NATYesOrNoUnknownType_Yes
629+ if responseOriginPairCount >= 1 {
630+ if allResponseOriginMatchRespFrom {
631+ t .PreserveSourceIPPortWhenDestNATMapping = NATYesOrNoUnknownType_Yes
632+ } else {
633+ t .PreserveSourceIPPortWhenDestNATMapping = NATYesOrNoUnknownType_No
634+ }
635+ } else {
636+ // Fallback: detect if we receive our own hairpin packets.
637+ // If RespFrom == ReqSentTo for all responses, the source address was rewritten (not preserved).
638+ allSendToMatchRespFrom := true
639+ respPairCount := 0
640+ for _ , tc := range transcripts {
641+ if tc .Resp == nil || tc .RespFrom == nil || tc .ReqSentTo == nil {
642+ continue
643+ }
644+ // Only count hairpin packets: responses that are binding requests (not binding responses)
645+ if tc .Resp .Type != stun .BindingRequest {
646+ continue
647+ }
648+ respPairCount ++
649+ if tc .RespFrom .String () != tc .ReqSentTo .String () {
650+ allSendToMatchRespFrom = false
651+ break
652+ }
653+ }
654+ switch {
655+ case respPairCount < 1 :
656+ t .PreserveSourceIPPortWhenDestNATMapping = NATYesOrNoUnknownType_Unknown
657+ case allSendToMatchRespFrom :
658+ t .PreserveSourceIPPortWhenDestNATMapping = NATYesOrNoUnknownType_No
659+ default :
660+ t .PreserveSourceIPPortWhenDestNATMapping = NATYesOrNoUnknownType_Yes
661+ }
623662 }
624663
625664 // PreserveSourcePortWhenSourceNATMapping: check if mapped port matches local source port
0 commit comments