├── go.mod ├── natpmp ├── doc.go ├── strings.go ├── client_test.go └── client.go ├── README.md ├── go.sum ├── .github └── workflows │ ├── staticcheck.yml │ └── linux.yml └── LICENSE /go.mod: -------------------------------------------------------------------------------- 1 | module inet.af/nat 2 | 3 | go 1.14 4 | 5 | require github.com/google/go-cmp v0.5.1 6 | -------------------------------------------------------------------------------- /natpmp/doc.go: -------------------------------------------------------------------------------- 1 | // Package natpmp implements the NAT Port Mapping Protocol (NAT-PMP) as 2 | // described in RFC 6886. 3 | package natpmp // import "inet.af/nat/natpmp" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nat [![Linux Test Status](https://github.com/inetaf/nat/workflows/Linux/badge.svg)](https://github.com/inetaf/nat/actions) [![GoDoc](https://godoc.org/inet.af/nat?status.svg)](https://godoc.org/inet.af/nat) 2 | 3 | A collection of Go networking packages for dealing with NATs and NAT traversal. 4 | 5 | **Warning:** there are no API stability guarantees at this time. 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= 2 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 4 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 5 | -------------------------------------------------------------------------------- /.github/workflows/staticcheck.yml: -------------------------------------------------------------------------------- 1 | name: staticcheck 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Set up Go 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: 1.14 20 | 21 | - name: Check out code 22 | uses: actions/checkout@v1 23 | 24 | - name: Print staticcheck version 25 | run: go run honnef.co/go/tools/cmd/staticcheck -version 26 | 27 | - name: Run staticcheck 28 | run: go run honnef.co/go/tools/cmd/staticcheck -- ./... 29 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | # 1.13 is required to use errors.Is. 16 | go-version: [1.13, 1.14] 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Set up Go 21 | uses: actions/setup-go@v1 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | id: go 25 | 26 | - name: Check out code into the Go module directory 27 | uses: actions/checkout@v1 28 | 29 | - name: Run tests on linux 30 | run: go test -race ./... 31 | -------------------------------------------------------------------------------- /natpmp/strings.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=Error -output=strings.go"; DO NOT EDIT. 2 | 3 | package natpmp 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[UnsupportedVersion-1] 12 | _ = x[NotAuthorized-2] 13 | _ = x[NetworkFailure-3] 14 | _ = x[OutOfResources-4] 15 | _ = x[UnsupportedOpcode-5] 16 | _ = x[success-0] 17 | } 18 | 19 | const _Error_name = "successUnsupportedVersionNotAuthorizedNetworkFailureOutOfResourcesUnsupportedOpcode" 20 | 21 | var _Error_index = [...]uint8{0, 7, 25, 38, 52, 66, 83} 22 | 23 | func (i Error) String() string { 24 | if i < 0 || i >= Error(len(_Error_index)-1) { 25 | return "Error(" + strconv.FormatInt(int64(i), 10) + ")" 26 | } 27 | return _Error_name[_Error_index[i]:_Error_index[i+1]] 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 The Inet.af AUTHORS. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Tailscale Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /natpmp/client_test.go: -------------------------------------------------------------------------------- 1 | package natpmp_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "github.com/google/go-cmp/cmp" 14 | "inet.af/nat/natpmp" 15 | ) 16 | 17 | func TestClientExternalAddress(t *testing.T) { 18 | t.Parallel() 19 | 20 | // Fixed data structures reused throughout tests. 21 | const op = 128 22 | 23 | var ( 24 | resNetworkFailure = []byte{ 25 | // An error header with no body. 26 | natpmp.Version, op, 0x00, uint8(natpmp.NetworkFailure), 27 | } 28 | 29 | resOK = []byte{ 30 | // Success response. 31 | natpmp.Version, op, 0x00, 0x00, 32 | // Duration since epoch. 33 | 0x00, 0x00, 0x01, 0xff, 34 | // External IP address. 35 | 192, 0, 2, 1, 36 | } 37 | 38 | ext = &natpmp.ExternalAddress{ 39 | SinceStartOfEpoch: 8*time.Minute + 31*time.Second, 40 | ExternalIP: net.IPv4(192, 0, 2, 1), 41 | } 42 | ) 43 | 44 | tests := []struct { 45 | name string 46 | fn serverFunc 47 | ext *natpmp.ExternalAddress 48 | err error 49 | }{ 50 | { 51 | name: "context deadline", 52 | err: context.DeadlineExceeded, 53 | }, 54 | { 55 | name: "short header", 56 | fn: func(_ []byte) []byte { 57 | return []byte{natpmp.Version, op, 0x00} 58 | }, 59 | err: io.ErrUnexpectedEOF, 60 | }, 61 | { 62 | name: "bad header version", 63 | fn: func(_ []byte) []byte { 64 | // Always expect version 0. 65 | return []byte{natpmp.Version + 1, op, 0x00, 0x00} 66 | }, 67 | err: natpmp.ErrProtocol, 68 | }, 69 | { 70 | name: "bad header op", 71 | fn: func(_ []byte) []byte { 72 | // Always expect a fixed response op. 73 | return []byte{natpmp.Version, op + 1, 0x00, 0x00} 74 | }, 75 | err: natpmp.ErrProtocol, 76 | }, 77 | { 78 | name: "short message", 79 | fn: func(_ []byte) []byte { 80 | return []byte{natpmp.Version, op, 0x00, 0x00, 0x00} 81 | }, 82 | err: io.ErrUnexpectedEOF, 83 | }, 84 | { 85 | name: "network failure", 86 | fn: func(_ []byte) []byte { return resNetworkFailure }, 87 | err: natpmp.NetworkFailure, 88 | }, 89 | { 90 | name: "success", 91 | fn: func(_ []byte) []byte { return resOK }, 92 | ext: ext, 93 | }, 94 | // In the retry tests, we simulate the first request being dropped so 95 | // the client must retry to receive a response. 96 | { 97 | name: "retry error", 98 | fn: func() serverFunc { 99 | var done bool 100 | return func(_ []byte) []byte { 101 | if !done { 102 | done = true 103 | return nil 104 | } 105 | 106 | return resNetworkFailure 107 | } 108 | }(), 109 | err: natpmp.NetworkFailure, 110 | }, 111 | { 112 | name: "retry success", 113 | fn: func() serverFunc { 114 | var done bool 115 | return func(_ []byte) []byte { 116 | if !done { 117 | done = true 118 | return nil 119 | } 120 | 121 | return resOK 122 | } 123 | }(), 124 | ext: ext, 125 | }, 126 | } 127 | 128 | for _, tt := range tests { 129 | tt := tt 130 | t.Run(tt.name, func(t *testing.T) { 131 | t.Parallel() 132 | 133 | var fn serverFunc 134 | if tt.fn != nil { 135 | fn = func(req []byte) []byte { 136 | // Each request is fixed. 137 | if diff := cmp.Diff([]byte{natpmp.Version, 0x00}, req); diff != "" { 138 | panicf("unexpected request (-want +got):\n%s", diff) 139 | } 140 | 141 | return tt.fn(req) 142 | } 143 | } 144 | 145 | c, done := testServer(t, fn) 146 | defer done() 147 | 148 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 149 | defer cancel() 150 | 151 | ext, _, err := c.ExternalAddress(ctx) 152 | if !errors.Is(err, tt.err) { 153 | t.Fatalf("unexpected error (-want +got):\n%s", cmp.Diff(tt.err, err)) 154 | } 155 | 156 | if diff := cmp.Diff(tt.ext, ext); diff != "" { 157 | t.Fatalf("unexpected external address (-want +got):\n%s", diff) 158 | } 159 | }) 160 | } 161 | } 162 | 163 | // A serverFunc is a function which can simulate a server's request/response 164 | // lifecycle. A nil return value indicates that no response will be sent. 165 | type serverFunc func(req []byte) (res []byte) 166 | 167 | func testServer(t *testing.T, fn serverFunc) (*natpmp.Client, func()) { 168 | t.Helper() 169 | 170 | // Create a local UDP server listener which will invoke fn for each request 171 | // to generate responses until the returned done function is invoked and 172 | // the context is canceled. 173 | pc, err := net.ListenPacket("udp4", "localhost:0") 174 | if err != nil { 175 | t.Fatalf("failed to bind local UDP server listener: %v", err) 176 | } 177 | 178 | ctx, cancel := context.WithCancel(context.Background()) 179 | 180 | var wg sync.WaitGroup 181 | wg.Add(1) 182 | 183 | go func() { 184 | defer wg.Done() 185 | 186 | if fn == nil { 187 | // Nothing to do. 188 | return 189 | } 190 | 191 | // Read client input and continue to send responses until the context 192 | // is canceled. 193 | b := make([]byte, 256) 194 | for { 195 | n, addr, err := pc.ReadFrom(b) 196 | if err != nil { 197 | if ctx.Err() != nil { 198 | // Halted via context. 199 | return 200 | } 201 | 202 | panicf("failed to read from client: %v", err) 203 | } 204 | 205 | if res := fn(b[:n]); res != nil { 206 | if _, err := pc.WriteTo(res, addr); err != nil { 207 | panicf("failed to write to client: %v", err) 208 | } 209 | } 210 | } 211 | }() 212 | 213 | // Point the test client at our server. 214 | c, err := natpmp.Dial(pc.LocalAddr().String()) 215 | if err != nil { 216 | t.Fatalf("failed to dial Client: %v", err) 217 | } 218 | 219 | return c, func() { 220 | // Unblock and halt the goroutine. 221 | cancel() 222 | _ = pc.SetReadDeadline(time.Unix(0, 1)) 223 | 224 | wg.Wait() 225 | _ = pc.Close() 226 | _ = c.Close() 227 | } 228 | } 229 | 230 | func panicf(format string, a ...interface{}) { 231 | panic(fmt.Sprintf(format, a...)) 232 | } 233 | -------------------------------------------------------------------------------- /natpmp/client.go: -------------------------------------------------------------------------------- 1 | package natpmp 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // Version is the expected protocol version for NAT-PMP. 15 | const Version = 0 16 | 17 | // ErrProtocol indicates that a NAT gateway returned a response that violates 18 | // the NAT-PMP protocol. 19 | var ErrProtocol = errors.New("natpmp: protocol error") 20 | 21 | //go:generate stringer -type=Error -output=strings.go 22 | 23 | // An Error is a NAT-PMP protocol result code which indicates that an operation 24 | // has failed. 25 | type Error int 26 | 27 | // Possible Errors as defined in RFC 6886, section 3.5. 28 | const ( 29 | // UnsupportedVersion indicates an unexpected NAT-PMP/PCP protocol version 30 | // was used to contact a NAT gateway. 31 | UnsupportedVersion Error = 1 32 | 33 | // NotAuthorized indicates that the NAT gateway supports mapping but the 34 | // mapping functionality is administratively disabled. 35 | NotAuthorized Error = 2 36 | 37 | // NetworkFailure indicates that the NAT gateway has not obtained a DHCP 38 | // lease and thus cannot provide an external IPv4 address. 39 | NetworkFailure Error = 3 40 | 41 | // OutOfResources indicates that the NAT gateway cannot create any more 42 | // mappings at this time. 43 | OutOfResources Error = 4 44 | 45 | // UnsupportedOpcode indicates that the NAT gateway does not recognize the 46 | // requested operation. 47 | UnsupportedOpcode Error = 5 48 | 49 | // success indicates a successful request. Although success is technically a 50 | // result code, we don't expose it directly to the user because a successful 51 | // operation returns nil error. 52 | success Error = 0 53 | ) 54 | 55 | // Error implements error. 56 | func (e Error) Error() string { 57 | return fmt.Sprintf("natpmp: result %d: %s", e, e.String()) 58 | } 59 | 60 | // A Client is a NAT-PMP client which can communicate with a NAT gateway. 61 | type Client struct { 62 | // The UDP socket and address used to communicate with a NAT gateway. 63 | mu sync.Mutex 64 | pc net.PacketConn 65 | gateway net.Addr 66 | } 67 | 68 | // Dial creates a Client which communicates with the NAT gateway specified by 69 | // addr. 70 | func Dial(addr string) (*Client, error) { 71 | gateway, err := net.ResolveUDPAddr("udp4", addr) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | pc, err := net.ListenPacket("udp4", ":0") 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | // TODO(mdlayher): create a second listener on 224.0.0.1:5350 to listen 82 | // to multicasts from a NAT gateway. 83 | 84 | return &Client{ 85 | pc: pc, 86 | gateway: gateway, 87 | }, nil 88 | } 89 | 90 | // Close closes the Client's underlying connection. 91 | func (c *Client) Close() error { 92 | return c.pc.Close() 93 | } 94 | 95 | // An ExternalAddress is the result of a Client's ExternalAddress method. 96 | type ExternalAddress struct { 97 | SinceStartOfEpoch time.Duration 98 | ExternalIP net.IP 99 | } 100 | 101 | // ExternalAddress returns external IP address information from a NAT gateway, 102 | // as described in RFC 4886, section 3.2. 103 | func (c *Client) ExternalAddress(ctx context.Context) (*ExternalAddress, net.Addr, error) { 104 | // This messages's request is always fixed, and the response always has a 105 | // fixed size and response opcode. See: 106 | // https://tools.ietf.org/html/rfc6886#section-3.2. 107 | const ( 108 | size = 12 109 | reqOp = 0x00 110 | resOp = 128 111 | ) 112 | 113 | b := make([]byte, size) 114 | n, addr, err := c.request(ctx, []byte{Version, reqOp}, b, resOp) 115 | if err != nil { 116 | return nil, nil, err 117 | } 118 | 119 | if n != size { 120 | return nil, nil, io.ErrUnexpectedEOF 121 | } 122 | 123 | // We allocated a buffer internally, no need to produce a copy of the data 124 | // for the output IP address. 125 | return &ExternalAddress{ 126 | SinceStartOfEpoch: time.Duration(binary.BigEndian.Uint32(b[4:8])) * time.Second, 127 | ExternalIP: b[8:12], 128 | }, addr, nil 129 | } 130 | 131 | // request serializes and implements backoff/retry for NAT-PMP request/response 132 | // interactions, as recommended by 133 | // https://tools.ietf.org/html/rfc6886#section-3.1. 134 | func (c *Client) request(ctx context.Context, req, res []byte, resOp int) (int, net.Addr, error) { 135 | c.mu.Lock() 136 | defer c.mu.Unlock() 137 | 138 | var wg sync.WaitGroup 139 | wg.Add(1) 140 | defer wg.Wait() 141 | 142 | // Either wait for the parent context to be canceled or for this function to 143 | // complete, and then unblock any outstanding reads and return control to 144 | // the caller. 145 | ctx, cancel := context.WithCancel(ctx) 146 | defer cancel() 147 | 148 | go func() { 149 | defer wg.Done() 150 | <-ctx.Done() 151 | _ = c.pc.SetDeadline(time.Unix(0, 1)) 152 | }() 153 | 154 | // Start with a 250ms delay on timeout error and double it up to 9 times 155 | // per the RFC. 156 | var nerr net.Error 157 | timeout := 250 * time.Millisecond 158 | for i := 0; i < 9; i++ { 159 | if err := ctx.Err(); err != nil { 160 | return 0, nil, err 161 | } 162 | 163 | // Send a request to the gateway and await its response or a timeout. 164 | if err := c.pc.SetReadDeadline(time.Now().Add(timeout)); err != nil { 165 | return 0, nil, err 166 | } 167 | 168 | if _, err := c.pc.WriteTo(req, c.gateway); err != nil { 169 | return 0, nil, err 170 | } 171 | 172 | n, addr, err := c.pc.ReadFrom(res) 173 | switch { 174 | case errors.As(err, &nerr) && nerr.Timeout(): 175 | // Was this timeout produced by context cancelation? If so, return 176 | // immediately. If not, double the timeout and retry. 177 | if err := ctx.Err(); err != nil { 178 | return 0, nil, err 179 | } 180 | 181 | timeout *= 2 182 | case err == nil: 183 | // Successful read, parse the header and verify the expected 184 | // response opcode. 185 | if err := checkHeader(res[:n], resOp); err != nil { 186 | return n, addr, err 187 | } 188 | 189 | return n, addr, nil 190 | default: 191 | // Unexpected error. 192 | return n, addr, err 193 | } 194 | } 195 | 196 | // TODO(mdlayher): implement net.Error.Timeout? 197 | return 0, nil, errors.New("natpmp: exhausted retries") 198 | } 199 | 200 | // checkHeader validates the header bytes of a NAT-PMP response. 201 | func checkHeader(b []byte, op int) error { 202 | if len(b) < 4 { 203 | return io.ErrUnexpectedEOF 204 | } 205 | 206 | if b[0] != Version { 207 | return fmt.Errorf("natpmp: unexpected protocol version: %d: %w", b[0], ErrProtocol) 208 | } 209 | if int(b[1]) != op { 210 | return fmt.Errorf("natpmp: unexpected response opcode: %d != %d: %w", b[1], op, ErrProtocol) 211 | } 212 | 213 | // Any non-zero value is an error and is wrapped in our Error type. 214 | if err := Error(binary.BigEndian.Uint16(b[2:4])); err != success { 215 | return err 216 | } 217 | 218 | return nil 219 | } 220 | --------------------------------------------------------------------------------