├── .gitignore ├── go.mod ├── Makefile ├── internal ├── logger.go └── ports.go ├── server ├── natprobe.service └── main.go ├── LICENSE ├── README.md ├── cmd └── natprobe │ └── main.go ├── go.sum └── client ├── client.go └── result.go /.gitignore: -------------------------------------------------------------------------------- 1 | cmd/natprobe/natprobe 2 | server/server 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.universe.tf/natprobe 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-logr/logr v0.1.0 7 | github.com/go-logr/zapr v0.1.2-0.20191218045755-6759ef05ca25 8 | github.com/urfave/cli/v2 v2.1.1 9 | go.uber.org/zap v1.13.0 10 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 11 | ) 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: 3 | go test ./... 4 | (cd cmd/natprobe && CGO_ENABLED=0 go build .) 5 | (cd server && CGO_ENABLED=0 go build .) 6 | 7 | .PHONY: deploy 8 | deploy: all 9 | scp server/server root@natprobe1:/opt/natprobe2 10 | scp server/natprobe.service root@natprobe1:/etc/systemd/system/natprobe.service 11 | ssh root@natprobe1 'mv -f /opt/natprobe2 /opt/natprobe && systemctl daemon-reload && systemctl restart natprobe' 12 | -------------------------------------------------------------------------------- /internal/logger.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/go-logr/logr" 8 | "github.com/go-logr/zapr" 9 | "go.uber.org/zap" 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | func NewLogger() logr.Logger { 14 | var ( 15 | logger *zap.Logger 16 | err error 17 | ) 18 | if isTerminal() { 19 | logger, err = zap.NewDevelopment() 20 | } else { 21 | logger, err = zap.NewProduction() 22 | } 23 | if err != nil { 24 | panic(fmt.Sprintf("Failed to initialize logger: %s", err)) 25 | } 26 | 27 | return zapr.NewLogger(logger) 28 | } 29 | 30 | func isTerminal() bool { 31 | _, err := unix.IoctlGetTermios(int(os.Stdout.Fd()), 0) 32 | return err == nil 33 | } 34 | -------------------------------------------------------------------------------- /server/natprobe.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Packet reflector for natprobe 3 | After=network-online.target 4 | Wants=network-online.target 5 | 6 | [Install] 7 | WantedBy=multi-user.target 8 | 9 | [Service] 10 | Type=simple 11 | ExecStart=/natprobe 12 | Restart=always 13 | 14 | # Security settings 15 | RootDirectory=/opt 16 | MountAPIVFS=false 17 | DynamicUser=true 18 | AmbientCapabilities=CAP_NET_BIND_SERVICE 19 | CapabilityBoundingSet=CAP_NET_BIND_SERVICE 20 | NoNewPrivileges=true 21 | SecureBits=no-setuid-fixup-locked noroot-locked keep-caps 22 | KeyringMode=private 23 | ProtectSystem=strict 24 | ProtectHome=true 25 | PrivateTmp=true 26 | PrivateDevices=true 27 | ProtectHostname=true 28 | ProtectKernelTunables=true 29 | ProtectKernelModules=true 30 | ProtectControlGroups=true 31 | RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 32 | RestrictNamespaces=true 33 | LockPersonality=true 34 | MemoryDenyWriteExecute=true 35 | RestrictRealtime=true 36 | RestrictSUIDSGID=true 37 | RemoveIPC=true 38 | PrivateMounts=true 39 | SystemCallFilter=@system-service @network-io 40 | SystemCallArchitectures=native 41 | -------------------------------------------------------------------------------- /internal/ports.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "sort" 4 | 5 | // Ports are the default ports that both client and server use for probing. 6 | var Ports = []int{ 7 | // One more random port in the IANA "Dynamic Ports" 8 | // range. Along with the other ports below, we cover each 9 | // of the 3 IANA port ranges ("Well Known", "Registered", 10 | // "Dynamic") with at least 2 ports each. 11 | 60000, 12 | 13 | // QUIC, likely to be open even on restrictive 14 | // networks. These are also two ports in the IANA "Well 15 | // Known Ports" range. 16 | 80, 443, 17 | 18 | // VPN protocols. Likely to be open on restrictive, but 19 | // business-friendly networks. 20 | 21 | // IKE (IPSec) 22 | 500, 23 | // L2TP over UDP 24 | 1701, 25 | // IPSec ESP over UDP 26 | 4500, 27 | // PPTP 28 | 1723, 29 | // OpenVPN 30 | 1194, 31 | // Wireguard 32 | 51820, 33 | 34 | // VOIP protocols. Likely to be open on restrictive, but 35 | // business-friendly networks. 36 | 37 | // STUN 38 | 3478, 39 | // SIP cleartext 40 | 5060, 41 | // SIP TLS 42 | 5061, 43 | } 44 | 45 | func init() { 46 | sort.Ints(Ports) 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dave Anderson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | natprobe is a Go toolkit to probe the behavior of NAT devices. It includes: 2 | 3 | - [`go.universe.tf/natprobe/client`](https://godoc.org/go.universe.tf/natprobe/client): 4 | a Go library to probe for the presence and behavior of a NAT 5 | device. 6 | - `go.universe.tf/natprobe/cli`: a thin CLI wrapper around the client 7 | library. 8 | - `go.universe.tf/natprobe/server`: a server that provides mapping 9 | information and probing services to the client library. 10 | 11 | By default, the client talk to two courtesy servers at 12 | `natprobe1.universe.tf` and `natprobe2.universe.tf`. 13 | 14 | Sample output from the CLI: 15 | 16 | ``` 17 | $ ./cmd/natprobe/natprobe 18 | NAT allocates a new ip:port for every unique 3-tuple (protocol, source ip, source ports). 19 | This is best practice for NAT devices. 20 | This makes NAT traversal easier. 21 | Firewall requires outbound traffic to an ip:port before allowing inbound traffic from that ip:port. 22 | This is common practice for NAT gateways. 23 | This makes NAT traversal more difficult. 24 | NAT seems to try and make the public port number match the LAN port number. 25 | NAT seems to only use one public IP for this client. 26 | ``` 27 | -------------------------------------------------------------------------------- /cmd/natprobe/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "time" 9 | 10 | cli "github.com/urfave/cli/v2" 11 | "go.universe.tf/natprobe/client" 12 | "go.universe.tf/natprobe/internal" 13 | ) 14 | 15 | var logger = internal.NewLogger() 16 | 17 | func main() { 18 | app := &cli.App{ 19 | Name: "natprobe", 20 | Usage: "detect and characterize NAT devices", 21 | Action: run, 22 | Flags: []cli.Flag{ 23 | // Probe servers 24 | &cli.StringSliceFlag{ 25 | Name: "servers", 26 | Usage: "prober servers to use", 27 | Value: cli.NewStringSlice("natprobe1.universe.tf.", "natprobe2.universe.tf."), 28 | }, 29 | &cli.IntSliceFlag{ 30 | Name: "ports", 31 | Usage: "UDP ports to probe", 32 | Value: cli.NewIntSlice(internal.Ports...), 33 | }, 34 | 35 | // DNS 36 | &cli.DurationFlag{ 37 | Name: "resolve-timeout", 38 | Usage: "DNS resolution timeout", 39 | Value: 3 * time.Second, 40 | }, 41 | 42 | // Mapping 43 | &cli.DurationFlag{ 44 | Name: "mapping-duration", 45 | Usage: "NAT mapping probe duration", 46 | Value: 3 * time.Second, 47 | }, 48 | &cli.DurationFlag{ 49 | Name: "mapping-tx-interval", 50 | Usage: "transmit interval for NAT mapping probes", 51 | Value: 200 * time.Millisecond, 52 | }, 53 | &cli.IntFlag{ 54 | Name: "mapping-sockets", 55 | Usage: "number of mapping sockets to use", 56 | Value: 3, 57 | }, 58 | 59 | // Firewall 60 | &cli.DurationFlag{ 61 | Name: "firewall-duration", 62 | Usage: "firewall probe duration", 63 | Value: 3 * time.Second, 64 | }, 65 | &cli.DurationFlag{ 66 | Name: "firewall-tx-interval", 67 | Usage: "transmit interval for firewall probes", 68 | Value: 50 * time.Millisecond, 69 | }, 70 | 71 | // Reporting 72 | &cli.BoolFlag{ 73 | Name: "print-results", 74 | Usage: "write the uninterpreted results to stdout", 75 | Value: false, 76 | }, 77 | &cli.BoolFlag{ 78 | Name: "anonymize-results", 79 | Usage: "anonymize IP addresses in results", 80 | Value: false, 81 | }, 82 | &cli.BoolFlag{ 83 | Name: "print-analysis", 84 | Usage: "write the interpreted analysis to stdout", 85 | Value: true, 86 | }, 87 | &cli.StringFlag{ 88 | Name: "format", 89 | Usage: "output format for results and analyses (text or json)", 90 | Value: "text", 91 | }, 92 | }, 93 | } 94 | app.Run(os.Args) 95 | } 96 | 97 | func run(c *cli.Context) error { 98 | var printer func(interface{}) 99 | switch c.String("format") { 100 | case "text": 101 | printer = textPrinter 102 | case "json": 103 | printer = jsonPrinter 104 | default: 105 | return fmt.Errorf("unknown --format value %q", c.String("format")) 106 | } 107 | 108 | opts := &client.Options{ 109 | ServerAddrs: c.StringSlice("servers"), 110 | Ports: c.IntSlice("ports"), 111 | ResolveDuration: c.Duration("resolve-timeout"), 112 | MappingDuration: c.Duration("mapping-duration"), 113 | MappingTransmitInterval: c.Duration("mapping-tx-interval"), 114 | MappingSockets: c.Int("mapping-sockets"), 115 | FirewallDuration: c.Duration("firewall-duration"), 116 | FirewallTransmitInterval: c.Duration("firewall-tx-interval"), 117 | } 118 | 119 | result, err := client.Probe(context.Background(), opts) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | if c.Bool("anonymize-results") { 125 | result.Anonymize() 126 | } 127 | if c.Bool("print-results") { 128 | printer(result) 129 | } 130 | if c.Bool("print-analysis") { 131 | printer(result.Analyze()) 132 | } 133 | return nil 134 | } 135 | 136 | func textPrinter(obj interface{}) { 137 | fmt.Println(obj) 138 | } 139 | 140 | func jsonPrinter(obj interface{}) { 141 | bs, err := json.MarshalIndent(obj, "", " ") 142 | if err != nil { 143 | panic(err) 144 | } 145 | fmt.Println(string(bs)) 146 | } 147 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "net" 9 | "os" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/go-logr/logr" 14 | "go.universe.tf/natprobe/internal" 15 | ) 16 | 17 | var ( 18 | ports = flag.String("ports", "", "UDP listener ports") 19 | ) 20 | 21 | func main() { 22 | flag.Parse() 23 | logger := internal.NewLogger() 24 | 25 | server, err := newServer(logger) 26 | if err != nil { 27 | logger.Error(err, "Failed to create server") 28 | os.Exit(1) 29 | } 30 | 31 | server.run() 32 | } 33 | 34 | func newServer(logger logr.Logger) (*server, error) { 35 | ips, err := publicIPs() 36 | if err != nil { 37 | return nil, fmt.Errorf("failed to enumerate local public IPs: %s", err) 38 | } 39 | if len(ips) < 2 { 40 | return nil, errors.New("not enough public IPs to provide a useful testing server") 41 | } 42 | 43 | ports, err := parsePorts() 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to parse listening ports: %s", err) 46 | } 47 | 48 | ret := &server{ 49 | logger: logger, 50 | } 51 | 52 | for _, ip := range ips { 53 | for _, port := range ports { 54 | addr := &net.UDPAddr{IP: ip, Port: port} 55 | conn, err := net.ListenUDP("udp4", addr) 56 | if err != nil { 57 | return nil, fmt.Errorf("failed to listen on %s: %s", addr, err) 58 | } 59 | ret.conns = append(ret.conns, conn) 60 | logger.Info("Created UDP listening port", "local-addr", addr.String()) 61 | } 62 | } 63 | 64 | return ret, nil 65 | } 66 | 67 | type server struct { 68 | conns []*net.UDPConn 69 | logger logr.Logger 70 | } 71 | 72 | func (s *server) run() { 73 | for _, conn := range s.conns { 74 | go s.handle(conn) 75 | } 76 | s.logger.Info("Startup complete") 77 | select {} 78 | } 79 | 80 | func (s *server) handle(conn *net.UDPConn) error { 81 | var buf [1500]byte 82 | for { 83 | n, addr, err := conn.ReadFromUDP(buf[:]) 84 | if err != nil { 85 | s.logger.Error(err, "Error reading from socket", "local-addr", conn.LocalAddr()) 86 | } 87 | if n != 180 { 88 | s.logger.Info("Ignoring packet of unexpected length", "local-addr", conn.LocalAddr(), "remote-addr", addr, "packet-size", n) 89 | continue 90 | } 91 | 92 | varyAddr, varyPort := buf[0]&1 != 0, buf[0]&2 != 0 93 | var respConn *net.UDPConn 94 | for _, c := range s.conns { 95 | myaddr := conn.LocalAddr().(*net.UDPAddr) 96 | uaddr := c.LocalAddr().(*net.UDPAddr) 97 | if uaddr.IP.Equal(myaddr.IP) == varyAddr { 98 | continue 99 | } 100 | if (uaddr.Port == myaddr.Port) == varyPort { 101 | continue 102 | } 103 | respConn = c 104 | break 105 | } 106 | 107 | copy(buf[:16], addr.IP.To16()) 108 | binary.BigEndian.PutUint16(buf[16:18], uint16(addr.Port)) 109 | if _, err = respConn.WriteToUDP(buf[:18], addr); err != nil { 110 | s.logger.Error(err, "Failed to send response", "remote-addr", addr) 111 | continue 112 | } 113 | 114 | s.logger.Info("Provided NAT mapping", "local-addr", respConn.LocalAddr(), "remote-addr", addr, "vary-addr", varyAddr, "vary-port", varyPort) 115 | } 116 | } 117 | 118 | func publicIPs() ([]net.IP, error) { 119 | ifaces, err := net.Interfaces() 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | var ret []net.IP 125 | 126 | for _, iface := range ifaces { 127 | addrs, err := iface.Addrs() 128 | if err != nil { 129 | return nil, err 130 | } 131 | for _, genAddr := range addrs { 132 | addr, ok := genAddr.(*net.IPNet) 133 | if !ok || addr.IP.To4() == nil || !addr.IP.IsGlobalUnicast() || isrfc1918(addr.IP) { 134 | continue 135 | } 136 | ret = append(ret, addr.IP.To4()) 137 | } 138 | } 139 | 140 | return ret, nil 141 | } 142 | 143 | func parsePorts() ([]int, error) { 144 | if *ports == "" { 145 | return internal.Ports, nil 146 | } 147 | 148 | ret := []int{} 149 | for _, port := range strings.Split(*ports, ",") { 150 | i, err := strconv.Atoi(port) 151 | if err != nil { 152 | return nil, err 153 | } 154 | ret = append(ret, i) 155 | } 156 | return ret, nil 157 | } 158 | 159 | func isrfc1918(ip net.IP) bool { 160 | ip = ip.To4() 161 | return ip[0] == 10 || 162 | (ip[0] == 172 && ip[1]&0xf0 == 16) || 163 | (ip[0] == 192 && ip[1] == 168) 164 | } 165 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg= 9 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 10 | github.com/go-logr/zapr v0.1.2-0.20191218045755-6759ef05ca25 h1:a0N5UadeK964bsLGetcxvDayHWI7/VpL5jh9wEVtRts= 11 | github.com/go-logr/zapr v0.1.2-0.20191218045755-6759ef05ca25/go.mod h1:AF8OAg3wCWqT+BZI3ED4Jpo8laY7NxVpa2VkqWu+IL4= 12 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 13 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 14 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 15 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 16 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 17 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 18 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 19 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 20 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 24 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 25 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 26 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 27 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 30 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 31 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 32 | github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= 33 | github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 34 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 35 | go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= 36 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 37 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 38 | go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= 39 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 40 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 41 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 42 | go.uber.org/zap v1.8.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 43 | go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= 44 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 45 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 46 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 47 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 48 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 49 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 50 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 51 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 52 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 53 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 54 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 55 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 57 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 59 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 60 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 61 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 62 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 63 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 64 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 66 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 67 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 68 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 69 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 70 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 71 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 72 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 73 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "fmt" 7 | "net" 8 | "time" 9 | 10 | "go.universe.tf/natprobe/internal" 11 | ) 12 | 13 | // Options configures the probe. All zero values are replaced with 14 | // sensible defaults. 15 | type Options struct { 16 | // The addresses of probe servers to use. 17 | ServerAddrs []string 18 | // The ports to probe on the probe servers. 19 | Ports []int 20 | 21 | // How long server name resolution can take. 22 | ResolveDuration time.Duration 23 | 24 | // How long the mapping phase takes. 25 | MappingDuration time.Duration 26 | // How frequently to send mapping probe packets for each socket 27 | // and destination. 28 | MappingTransmitInterval time.Duration 29 | // The number of sockets to use for probing. 30 | MappingSockets int 31 | 32 | // How long the firewall probing phase takes. 33 | FirewallDuration time.Duration 34 | // How frequently to send firewal probe packets for each socket. 35 | FirewallTransmitInterval time.Duration 36 | } 37 | 38 | func (o *Options) addDefaults() { 39 | if len(o.ServerAddrs) == 0 { 40 | o.ServerAddrs = []string{"natprobe1-4.universe.tf.", "natprobe2-4.universe.tf."} 41 | } 42 | if len(o.Ports) == 0 { 43 | o.Ports = internal.Ports 44 | } 45 | if o.ResolveDuration == 0 { 46 | o.ResolveDuration = 3 * time.Second 47 | } 48 | if o.MappingDuration == 0 { 49 | o.MappingDuration = 3 * time.Second 50 | } 51 | if o.MappingTransmitInterval == 0 { 52 | o.MappingTransmitInterval = 200 * time.Millisecond 53 | } 54 | if o.MappingSockets == 0 { 55 | o.MappingSockets = 3 56 | } 57 | if o.FirewallDuration == 0 { 58 | o.FirewallDuration = 3 * time.Second 59 | } 60 | if o.FirewallTransmitInterval == 0 { 61 | o.FirewallTransmitInterval = 50 * time.Millisecond 62 | } 63 | } 64 | 65 | // Probe probes the NAT behavior between the local machine and remote probe servers. 66 | func Probe(ctx context.Context, opts *Options) (*Result, error) { 67 | if opts == nil { 68 | opts = &Options{} 69 | } 70 | opts.addDefaults() 71 | 72 | addrs, err := net.InterfaceAddrs() 73 | if err != nil { 74 | return nil, fmt.Errorf("enumerating local addresses: %s", err) 75 | } 76 | var localIPs []net.IP 77 | for _, addr := range addrs { 78 | if ipnet, ok := addr.(*net.IPNet); ok { 79 | if ipnet.IP.To4() != nil { 80 | localIPs = append(localIPs, ipnet.IP) 81 | } 82 | } 83 | } 84 | 85 | // Assemble destination UDP addresses. 86 | ips, err := resolveServerAddrs(ctx, opts.ServerAddrs, opts.ResolveDuration) 87 | if err != nil { 88 | return nil, err 89 | } 90 | dests := dests(ips, opts.Ports) 91 | 92 | // Channel for the mapping probe to pass a working server to the firewall. 93 | var ( 94 | workingAddr = make(chan *net.UDPAddr, 1) 95 | firewallDone = make(chan error) 96 | firewall *FirewallProbe 97 | ) 98 | 99 | // If we get any successful mapping response, use that address for 100 | // firewall probing. 101 | go func() { 102 | fw, err := probeFirewall(ctx, workingAddr, opts.FirewallDuration, opts.FirewallTransmitInterval) 103 | firewall = fw 104 | firewallDone <- err 105 | }() 106 | 107 | // Probe the NAT for its mapping behavior. 108 | probes, err := probeMapping(ctx, dests, opts.MappingSockets, opts.MappingDuration, opts.MappingTransmitInterval, workingAddr) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | if err = <-firewallDone; err != nil { 114 | return nil, err 115 | } 116 | 117 | return &Result{ 118 | LocalIPs: localIPs, 119 | MappingProbes: probes, 120 | FirewallProbes: firewall, 121 | }, nil 122 | } 123 | 124 | func dests(ips []net.IP, ports []int) []*net.UDPAddr { 125 | var ret []*net.UDPAddr 126 | for _, ip := range ips { 127 | for _, port := range ports { 128 | ret = append(ret, &net.UDPAddr{IP: ip, Port: port}) 129 | } 130 | } 131 | return ret 132 | } 133 | 134 | func probeFirewall(ctx context.Context, workingAddr chan *net.UDPAddr, duration time.Duration, txInterval time.Duration) (*FirewallProbe, error) { 135 | dest := <-workingAddr 136 | if dest == nil { 137 | return nil, fmt.Errorf("no working server addresses available for firewall probing") 138 | } 139 | conn, err := net.ListenUDP("udp4", &net.UDPAddr{}) 140 | if err != nil { 141 | return nil, err 142 | } 143 | defer conn.Close() 144 | 145 | ctx, cancel := context.WithTimeout(ctx, duration) 146 | defer cancel() 147 | 148 | deadline, ok := ctx.Deadline() 149 | if !ok { 150 | panic("deadline unexpectedly not set in context") 151 | } 152 | if err = conn.SetReadDeadline(deadline); err != nil { 153 | return nil, err 154 | } 155 | 156 | go transmit(ctx, conn, []*net.UDPAddr{dest}, txInterval, true) 157 | 158 | var ( 159 | ret = FirewallProbe{ 160 | Local: copyUDPAddr(conn.LocalAddr().(*net.UDPAddr)), 161 | Remote: copyUDPAddr(dest), 162 | } 163 | buf [1500]byte 164 | seen = map[string]bool{} 165 | ) 166 | for { 167 | n, addr, err := conn.ReadFromUDP(buf[:]) 168 | if err != nil { 169 | if nerr, ok := err.(net.Error); ok && nerr.Timeout() { 170 | return &ret, nil 171 | } 172 | return nil, err 173 | } 174 | 175 | if n != 18 { 176 | continue 177 | } 178 | 179 | if !seen[addr.String()] { 180 | ret.Received = append(ret.Received, addr) 181 | seen[addr.String()] = true 182 | } 183 | } 184 | } 185 | 186 | func probeMapping(ctx context.Context, dests []*net.UDPAddr, sockets int, duration time.Duration, txInterval time.Duration, workingAddr chan *net.UDPAddr) ([]*MappingProbe, error) { 187 | defer close(workingAddr) 188 | 189 | ctx, cancel := context.WithTimeout(ctx, duration) 190 | defer cancel() 191 | 192 | type result struct { 193 | probes []*MappingProbe 194 | err error 195 | } 196 | 197 | done := make(chan result) 198 | 199 | for i := 0; i < sockets; i++ { 200 | go func() { 201 | res, err := probeOneMapping(ctx, dests, txInterval, workingAddr) 202 | done <- result{probes: res, err: err} 203 | }() 204 | } 205 | 206 | var ret []*MappingProbe 207 | for i := 0; i < sockets; i++ { 208 | res := <-done 209 | if res.err != nil { 210 | return nil, res.err 211 | } 212 | ret = append(ret, res.probes...) 213 | } 214 | 215 | return ret, nil 216 | } 217 | 218 | func probeOneMapping(ctx context.Context, dests []*net.UDPAddr, txInterval time.Duration, workingAddr chan *net.UDPAddr) (ret []*MappingProbe, err error) { 219 | conn, err := net.ListenUDP("udp4", &net.UDPAddr{}) 220 | if err != nil { 221 | return nil, err 222 | } 223 | defer conn.Close() 224 | 225 | ctx, cancel := context.WithCancel(ctx) 226 | defer cancel() 227 | 228 | deadline, ok := ctx.Deadline() 229 | if !ok { 230 | panic("deadline unexpectedly not set in context") 231 | } 232 | if err = conn.SetReadDeadline(deadline); err != nil { 233 | return nil, err 234 | } 235 | 236 | go transmit(ctx, conn, dests, txInterval, false) 237 | 238 | var ( 239 | buf [1500]byte 240 | seen = map[string]bool{} 241 | ) 242 | 243 | seenByDest := map[string]bool{} 244 | for { 245 | n, addr, err := conn.ReadFromUDP(buf[:]) 246 | if err != nil { 247 | if nerr, ok := err.(net.Error); ok && nerr.Timeout() { 248 | for _, dest := range dests { 249 | if !seenByDest[dest.String()] { 250 | ret = append(ret, &MappingProbe{ 251 | Local: copyUDPAddr(conn.LocalAddr().(*net.UDPAddr)), 252 | Remote: copyUDPAddr(dest), 253 | Timeout: true, 254 | }) 255 | } 256 | } 257 | return ret, nil 258 | } 259 | return nil, err 260 | } 261 | 262 | if n != 18 { 263 | continue 264 | } 265 | 266 | mapped := &net.UDPAddr{ 267 | IP: net.IP(buf[:16]), 268 | Port: int(binary.BigEndian.Uint16(buf[16:18])), 269 | } 270 | 271 | probe := &MappingProbe{ 272 | Local: copyUDPAddr(conn.LocalAddr().(*net.UDPAddr)), 273 | Mapped: copyUDPAddr(mapped), 274 | Remote: copyUDPAddr(addr), 275 | } 276 | if !seen[probe.key()] { 277 | ret = append(ret, probe) 278 | seen[probe.key()] = true 279 | seenByDest[addr.String()] = true 280 | select { 281 | case workingAddr <- copyUDPAddr(addr): 282 | default: 283 | } 284 | } 285 | } 286 | } 287 | 288 | func transmit(ctx context.Context, conn *net.UDPConn, dests []*net.UDPAddr, txInterval time.Duration, cycle bool) { 289 | var req [180]byte 290 | done := make(chan struct{}) 291 | for _, dest := range dests { 292 | go func(dest *net.UDPAddr) { 293 | defer func() { done <- struct{}{} }() 294 | 295 | for { 296 | if cycle { 297 | req[0] = (req[0] + 1) % 4 298 | } 299 | if _, err := conn.WriteToUDP(req[:], dest); err != nil { 300 | // TODO: log, somehow... 301 | } 302 | select { 303 | case <-ctx.Done(): 304 | return 305 | case <-time.After(txInterval): 306 | } 307 | } 308 | }(dest) 309 | } 310 | 311 | for range dests { 312 | <-done 313 | } 314 | } 315 | 316 | func resolveServerAddrs(ctx context.Context, addrs []string, timeout time.Duration) (ips []net.IP, err error) { 317 | ctx, cancel := context.WithTimeout(ctx, timeout) 318 | defer cancel() 319 | 320 | for _, addr := range addrs { 321 | results, err := net.DefaultResolver.LookupIPAddr(ctx, addr) 322 | if err != nil { 323 | return nil, err 324 | } 325 | 326 | for _, result := range results { 327 | ip := result.IP.To4() 328 | if ip == nil { 329 | continue 330 | } 331 | ips = append(ips, ip) 332 | } 333 | } 334 | return ips, nil 335 | } 336 | 337 | func copyUDPAddr(a *net.UDPAddr) *net.UDPAddr { 338 | return &net.UDPAddr{ 339 | IP: append(net.IP(nil), a.IP...), 340 | Port: a.Port, 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /client/result.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // Result is the raw, uninterpreted result of a probe. 13 | type Result struct { 14 | LocalIPs []net.IP 15 | MappingProbes []*MappingProbe 16 | FirewallProbes *FirewallProbe 17 | } 18 | 19 | // MappingProbe is the outcome of a single NAT mapping discovery attempt. 20 | type MappingProbe struct { 21 | Local *net.UDPAddr 22 | Mapped *net.UDPAddr 23 | Remote *net.UDPAddr 24 | Timeout bool 25 | } 26 | 27 | func (p MappingProbe) key() string { 28 | return fmt.Sprintf("%s %s %s %t", p.Local, p.Mapped, p.Remote, p.Timeout) 29 | } 30 | 31 | // FirewallProbe is the outcome of a firewall state probe. 32 | type FirewallProbe struct { 33 | Local *net.UDPAddr 34 | Remote *net.UDPAddr 35 | Received []*net.UDPAddr 36 | } 37 | 38 | // String returns a human-readable description of the probe results. 39 | func (r *Result) String() string { 40 | if len(r.MappingProbes) == 0 { 41 | return "No data (did the probe fail?)" 42 | } 43 | 44 | var b bytes.Buffer 45 | 46 | b.WriteString("Local IPs on the client:\n") 47 | for _, ip := range r.LocalIPs { 48 | fmt.Fprintf(&b, " %s\n", ip) 49 | } 50 | 51 | b.WriteString("Mapping probes:\n") 52 | for _, probe := range r.MappingProbes { 53 | if probe.Timeout { 54 | fmt.Fprintf(&b, " %s -> ??? -> %s (timeout)\n", probe.Local, probe.Remote) 55 | } else { 56 | fmt.Fprintf(&b, " %s -> %s -> %s\n", probe.Local, probe.Mapped, probe.Remote) 57 | } 58 | } 59 | 60 | if r.FirewallProbes == nil { 61 | fmt.Fprintf(&b, "No firewall probe data.\n") 62 | } else { 63 | fmt.Fprintf(&b, "Firewall probe with outbound traffic %s -> %s\n", r.FirewallProbes.Local, r.FirewallProbes.Remote) 64 | for _, addr := range r.FirewallProbes.Received { 65 | fmt.Fprintf(&b, " %s\n", addr) 66 | } 67 | } 68 | 69 | return b.String() 70 | } 71 | 72 | // Anonymize replace all IP addresses in the results with generated IPs. 73 | func (r *Result) Anonymize() { 74 | ips := map[string]net.IP{} 75 | a, b := byte(1), byte(1) 76 | 77 | anonymize := func(ip net.IP) net.IP { 78 | if len(ip) == 0 || ip.IsUnspecified() { 79 | // Nothing to anonymize. 80 | return ip 81 | } 82 | 83 | if ret := ips[ip.String()]; ret != nil { 84 | return ret 85 | } 86 | ret := net.IPv4(a, a, b, b) 87 | b++ 88 | if b == 0 { 89 | a++ 90 | } 91 | ips[ip.String()] = ret 92 | return ret 93 | } 94 | 95 | for i, ip := range r.LocalIPs { 96 | r.LocalIPs[i] = anonymize(ip) 97 | } 98 | for _, probe := range r.MappingProbes { 99 | probe.Local.IP = anonymize(probe.Local.IP) 100 | probe.Mapped.IP = anonymize(probe.Mapped.IP) 101 | probe.Remote.IP = anonymize(probe.Remote.IP) 102 | } 103 | if r.FirewallProbes == nil { 104 | return 105 | } 106 | r.FirewallProbes.Local.IP = anonymize(r.FirewallProbes.Local.IP) 107 | r.FirewallProbes.Remote.IP = anonymize(r.FirewallProbes.Remote.IP) 108 | for _, addr := range r.FirewallProbes.Received { 109 | addr.IP = anonymize(addr.IP) 110 | } 111 | } 112 | 113 | // Analyze distills raw results into an Analysis. 114 | func (r *Result) Analyze() *Analysis { 115 | return &Analysis{ 116 | NoData: noData(r), 117 | NoNAT: noNAT(r), 118 | MappingVariesByDestIP: mappingVariesByDestIP(r), 119 | MappingVariesByDestPort: mappingVariesByDestPort(r), 120 | FirewallEnforcesDestIP: firewallEnforcesDestIP(r), 121 | FirewallEnforcesDestPort: firewallEnforcesDestPort(r), 122 | MappingPreservesSourcePort: mappingPreservesSourcePort(r), 123 | MultiplePublicIPs: multiplePublicIPs(r), 124 | FilteredEgress: filteredEgress(r), 125 | } 126 | } 127 | 128 | func noData(r *Result) bool { 129 | if len(r.MappingProbes) == 0 { 130 | return true 131 | } 132 | for _, probe := range r.MappingProbes { 133 | if !probe.Timeout { 134 | return false 135 | } 136 | } 137 | return true 138 | } 139 | 140 | func noNAT(r *Result) bool { 141 | ips := map[string]bool{} 142 | for _, ip := range r.LocalIPs { 143 | ips[ip.String()] = true 144 | } 145 | for _, probe := range r.MappingProbes { 146 | if probe.Timeout { 147 | continue 148 | } 149 | if !ips[probe.Mapped.IP.String()] { 150 | return false 151 | } 152 | } 153 | 154 | return true 155 | } 156 | 157 | func mappingVariesByDestIP(r *Result) bool { 158 | var ( 159 | local string 160 | remoteIP net.IP 161 | mappedIP net.IP 162 | mappedPort int 163 | ) 164 | 165 | for _, probe := range r.MappingProbes { 166 | if probe.Timeout { 167 | continue 168 | } 169 | if probe.Local.String() != local { 170 | local = probe.Local.String() 171 | remoteIP = probe.Remote.IP 172 | mappedIP = probe.Mapped.IP 173 | mappedPort = probe.Mapped.Port 174 | continue 175 | } 176 | if probe.Remote.IP.Equal(remoteIP) { 177 | continue 178 | } 179 | if !probe.Mapped.IP.Equal(mappedIP) || probe.Mapped.Port != mappedPort { 180 | return true 181 | } 182 | } 183 | return false 184 | } 185 | 186 | func mappingVariesByDestPort(r *Result) bool { 187 | var ( 188 | local string 189 | remotePort int 190 | mappedIP net.IP 191 | mappedPort int 192 | ) 193 | 194 | for _, probe := range r.MappingProbes { 195 | if probe.Timeout { 196 | continue 197 | } 198 | if probe.Local.String() != local { 199 | local = probe.Local.String() 200 | remotePort = probe.Remote.Port 201 | mappedIP = probe.Mapped.IP 202 | mappedPort = probe.Mapped.Port 203 | continue 204 | } 205 | if probe.Remote.Port == remotePort { 206 | continue 207 | } 208 | if !probe.Mapped.IP.Equal(mappedIP) || probe.Mapped.Port != mappedPort { 209 | return true 210 | } 211 | } 212 | return false 213 | } 214 | 215 | func mappingVariesBy(r *Result, keyFunc func(*MappingProbe) string) bool { 216 | var ( 217 | key string 218 | mappedIP net.IP 219 | mappedPort int 220 | ) 221 | for _, probe := range r.MappingProbes { 222 | if probe.Timeout { 223 | continue 224 | } 225 | if mappedIP == nil { 226 | key = keyFunc(probe) 227 | mappedIP = probe.Mapped.IP 228 | mappedPort = probe.Mapped.Port 229 | continue 230 | } 231 | 232 | if keyFunc(probe) == key { 233 | continue 234 | } 235 | if !mappedIP.Equal(probe.Mapped.IP) || probe.Mapped.Port != mappedPort { 236 | return true 237 | } 238 | } 239 | return false 240 | } 241 | 242 | func firewallEnforcesDestIP(r *Result) bool { 243 | if r.FirewallProbes == nil { 244 | return false 245 | } 246 | outIP := r.FirewallProbes.Remote.IP 247 | for _, recv := range r.FirewallProbes.Received { 248 | if !recv.IP.Equal(outIP) { 249 | return false 250 | } 251 | } 252 | 253 | return true 254 | } 255 | 256 | func firewallEnforcesDestPort(r *Result) bool { 257 | if r.FirewallProbes == nil { 258 | return false 259 | } 260 | outPort := r.FirewallProbes.Remote.Port 261 | for _, recv := range r.FirewallProbes.Received { 262 | if recv.Port != outPort { 263 | return false 264 | } 265 | } 266 | return true 267 | } 268 | 269 | func mappingPreservesSourcePort(r *Result) bool { 270 | total, preserved := 0, 0 271 | for _, probe := range r.MappingProbes { 272 | if probe.Timeout { 273 | continue 274 | } 275 | total++ 276 | if probe.Local.Port == probe.Mapped.Port { 277 | preserved++ 278 | } 279 | } 280 | 281 | // Consider the NAT port-preserving if >80% of probes have 282 | // preserved ports. 283 | return (float64(preserved) / float64(total)) >= 0.8 284 | } 285 | 286 | func multiplePublicIPs(r *Result) bool { 287 | ips := map[string]bool{} 288 | for _, probe := range r.MappingProbes { 289 | if probe.Timeout { 290 | continue 291 | } 292 | ips[probe.Mapped.IP.String()] = true 293 | } 294 | return len(ips) > 1 295 | } 296 | 297 | func filteredEgress(r *Result) []int { 298 | working := map[int]bool{} 299 | for _, probe := range r.MappingProbes { 300 | if !probe.Timeout { 301 | working[probe.Remote.Port] = true 302 | } 303 | } 304 | ret := []int{} 305 | for _, probe := range r.MappingProbes { 306 | if probe.Timeout && !working[probe.Remote.Port] { 307 | ret = append(ret, probe.Remote.Port) 308 | working[probe.Remote.Port] = true 309 | } 310 | } 311 | sort.Ints(ret) 312 | return ret 313 | } 314 | 315 | // Analysis is a high level "feature" analysis of NAT behavior. 316 | type Analysis struct { 317 | // There is no data to analyze. 318 | NoData bool 319 | // There is no NAT, at least one local IP appears to be a public IP. 320 | NoNAT bool 321 | // Assigned public ip:port depends on the destination IP. 322 | MappingVariesByDestIP bool 323 | // Assigned public ip:port depends on the destination port. 324 | MappingVariesByDestPort bool 325 | // Firewall requires outbound traffic to an IP before allowing 326 | // inbound traffic from that IP. 327 | FirewallEnforcesDestIP bool 328 | // Firewall requires outbound traffic to a port before allowing 329 | // inbound traffic from that port. 330 | FirewallEnforcesDestPort bool 331 | // Assigned public port tries to be the same as the LAN port. 332 | MappingPreservesSourcePort bool 333 | // Observed multiple assigned public IPs. 334 | MultiplePublicIPs bool 335 | // Outbound probes that didn't see a response, indicating outbound 336 | // filtering. 337 | FilteredEgress []int 338 | } 339 | 340 | // String returns a human-readable description of the analysis. 341 | func (a *Analysis) String() string { 342 | if a.NoData { 343 | return "Probing got no useful data at all. Either the probe servers are down, or extremely strict UDP filtering is in place on your LAN." 344 | } 345 | 346 | if a.NoNAT { 347 | return "There doesn't seem to be a NAT between you and the internet. Good for you!" 348 | } 349 | 350 | ret := []string{} 351 | 352 | switch { 353 | case a.MappingVariesByDestPort && a.MappingVariesByDestIP: 354 | ret = append(ret, `NAT allocates a new ip:port for every unique 5-tuple (protocol, source ip, source port, destination ip, destination port). 355 | This makes NAT traversal more difficult.`) 356 | case a.MappingVariesByDestIP: 357 | ret = append(ret, `NAT allocates a new ip:port for every unique IP 4-tuple (protocol, source ip, source port, destination ip). 358 | This makes NAT traversal more difficult.`) 359 | case a.MappingVariesByDestPort: 360 | ret = append(ret, `NAT allocates a new ip:port for every unique port 4-tuple (protocol, source ip, source port, destination port). 361 | This is unusual! 362 | This makes NAT traversal more difficult.`) 363 | default: 364 | ret = append(ret, `NAT allocates a new ip:port for every unique 3-tuple (protocol, source ip, source ports). 365 | This is best practice for NAT devices. 366 | This makes NAT traversal easier.`) 367 | } 368 | 369 | switch { 370 | case a.FirewallEnforcesDestIP && a.FirewallEnforcesDestPort: 371 | ret = append(ret, `Firewall requires outbound traffic to an ip:port before allowing inbound traffic from that ip:port. 372 | This is common practice for NAT gateways. 373 | This makes NAT traversal more difficult.`) 374 | case a.FirewallEnforcesDestIP: 375 | ret = append(ret, `Firewall requires outbound traffic to an ip before allowing inbound traffic from that ip, but the ports don't have to match. 376 | This makes NAT traversal more difficult.`) 377 | case a.FirewallEnforcesDestPort: 378 | ret = append(ret, `Firewall requires outbound traffic to a port before allowing inbound traffic from that port, but the IPs don't have to match. 379 | This is unusual! 380 | This makes NAT traversal more difficult.`) 381 | default: 382 | ret = append(ret, `Firewall allows inbound traffic from any source, with no prerequisites. 383 | This is best practice for "traversal-friendly" NAT devices.`) 384 | } 385 | 386 | if a.MappingPreservesSourcePort { 387 | ret = append(ret, `NAT seems to try and make the public port number match the LAN port number.`) 388 | } else { 389 | ret = append(ret, `NAT seems to randomize the public port when allocating a new mapping.`) 390 | } 391 | 392 | if a.MultiplePublicIPs { 393 | ret = append(ret, `NAT seems to use different public IPs for different mappings. 394 | This makes NAT traversal more difficult.`) 395 | } else { 396 | ret = append(ret, `NAT seems to only use one public IP for this client.`) 397 | } 398 | 399 | switch len(a.FilteredEgress) { 400 | case 0: 401 | case 1: 402 | ret = append(ret, fmt.Sprintf("Outbound UDP port %d seems to be blocked.", a.FilteredEgress[0])) 403 | default: 404 | ports := []string{} 405 | for _, p := range a.FilteredEgress { 406 | ports = append(ports, strconv.Itoa(p)) 407 | } 408 | ret = append(ret, fmt.Sprintf("Outbound UDP ports %s seem to be blocked.", strings.Join(ports, ", "))) 409 | } 410 | 411 | return strings.Join(ret, "\n") 412 | } 413 | --------------------------------------------------------------------------------