// Package portmapper is a minimal NAT-PMP (RFC 6886) client for requesting a // UDP port mapping from a home gateway.
package portmapper import ( ) const ( natPMPPort = 5351 natPMPVersion = 0 natPMPPublicAddr = 0 natPMPMapUDP = 1 natPMPResponse = 0x80 ) // NATPMPClient is a minimal NAT-PMP client. type NATPMPClient struct { Gateway netip.Addr Port uint16 Timeout time.Duration } // Mapping is a UDP port mapping returned by a gateway. type Mapping struct { ExternalAddr netip.AddrPort Lifetime time.Duration } // MapUDP maps internalPort through the NAT-PMP gateway, requesting the given // lifetime. A lifetime of 0 deletes the mapping (RFC 6886 ยง3.4): the gateway // removes any mapping for internalPort and returns a zero Mapping. func ( NATPMPClient) ( context.Context, uint16, uint16, time.Duration) (Mapping, error) { if == 0 { return Mapping{}, errors.New("portmapper: zero internal port") } := uint32( / time.Second) , := .ExternalIPv4() if != nil { return Mapping{}, } := make([]byte, 12) [0] = natPMPVersion [1] = natPMPMapUDP binary.BigEndian.PutUint16([4:6], ) binary.BigEndian.PutUint16([6:8], ) binary.BigEndian.PutUint32([8:12], ) , := .roundTrip(, , 16) if != nil { return Mapping{}, } if [1] != natPMPResponse|natPMPMapUDP { return Mapping{}, fmt.Errorf("portmapper: unexpected nat-pmp opcode %d", [1]) } if := natPMPResult([2:4]); != nil { return Mapping{}, } if := binary.BigEndian.Uint16([8:10]); != { return Mapping{}, fmt.Errorf("portmapper: mapped internal port %d, want %d", , ) } := binary.BigEndian.Uint16([10:12]) := time.Duration(binary.BigEndian.Uint32([12:16])) * time.Second return Mapping{ ExternalAddr: netip.AddrPortFrom(, ), Lifetime: , }, nil } // ExternalIPv4 returns the public IPv4 address reported by the gateway. func ( NATPMPClient) ( context.Context) (netip.Addr, error) { , := .roundTrip(, []byte{natPMPVersion, natPMPPublicAddr}, 12) if != nil { return netip.Addr{}, } if [1] != natPMPResponse|natPMPPublicAddr { return netip.Addr{}, fmt.Errorf("portmapper: unexpected nat-pmp opcode %d", [1]) } if := natPMPResult([2:4]); != nil { return netip.Addr{}, } return netip.AddrFrom4([4]byte([8:12])), nil } func ( NATPMPClient) ( context.Context, []byte, int) ([]byte, error) { := .Gateway if !.IsValid() || !.Is4() { return nil, errors.New("portmapper: invalid nat-pmp gateway") } if == nil { = context.Background() } := .Timeout if <= 0 { = 2 * time.Second } := .Port if == 0 { = natPMPPort } := net.Dialer{} , := .DialContext(, "udp", netip.AddrPortFrom(, ).String()) if != nil { return nil, fmt.Errorf("portmapper: dial nat-pmp gateway: %w", ) } defer .Close() , := .Deadline() if ! { = time.Now().Add() } _ = .SetDeadline() if , := .Write(); != nil { return nil, fmt.Errorf("portmapper: write nat-pmp request: %w", ) } := make([]byte, ) , := .Read() if != nil { return nil, fmt.Errorf("portmapper: read nat-pmp response: %w", ) } if != { return nil, fmt.Errorf("portmapper: short nat-pmp response %d, want %d", , ) } if [0] != natPMPVersion { return nil, fmt.Errorf("portmapper: unexpected nat-pmp version %d", [0]) } return , nil } func natPMPResult( []byte) error { := binary.BigEndian.Uint16() if == 0 { return nil } return fmt.Errorf("portmapper: nat-pmp result code %d", ) }