├── doc └── screen.png ├── client-gui ├── rsrc_386.syso ├── rsrc_amd64.syso ├── client.manifest └── client.go ├── go.mod ├── go.sum ├── .builds ├── mac.yml ├── windows.yml └── linux.yml ├── README.md ├── client └── client.go ├── common.go ├── relay └── relay.go └── server └── server.go /doc/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delthas/punch-check/HEAD/doc/screen.png -------------------------------------------------------------------------------- /client-gui/rsrc_386.syso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delthas/punch-check/HEAD/client-gui/rsrc_386.syso -------------------------------------------------------------------------------- /client-gui/rsrc_amd64.syso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delthas/punch-check/HEAD/client-gui/rsrc_amd64.syso -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/delthas/punch-check 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1 7 | github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4 // indirect 8 | golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4 // indirect 9 | gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1 h1:/QwQcwWVOQXcoNuV9tHx30gQ3q7jCE/rKcGjwzsa5tg= 2 | github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= 3 | github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4 h1:5BmtGkQbch91lglMHQ9JIDGiYCL3kBRBA0ItZTvOcEI= 4 | github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA= 5 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 6 | golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4 h1:c1Sgqkh8v6ZxafNGG64r8C8UisIW2TKMJN8P86tKjr0= 7 | golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 8 | gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= 9 | gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= 10 | -------------------------------------------------------------------------------- /client-gui/client.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | PerMonitorV2, PerMonitor 12 | True 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.builds/mac.yml: -------------------------------------------------------------------------------- 1 | image: archlinux 2 | packages: 3 | - git 4 | - go 5 | sources: 6 | - git@github.com:delthas/punch-check.git 7 | secrets: 8 | - 63d7a0ac-2635-4d42-9709-efcd2cf74858 # ssh deploy key 9 | tasks: 10 | - build: | 11 | cd punch-check 12 | GOOS=darwin go build -ldflags "-s -w" -v -o punch-check ./client 13 | GOOS=darwin go build -ldflags "-s -w" -v -o punch-check-relay ./relay 14 | GOOS=darwin go build -ldflags "-s -w" -v -o punch-check-server ./server 15 | - deploy: | 16 | cd punch-check 17 | ssh -p 2222 -o StrictHostKeyChecking=no -q user@delthas.fr 'mkdir -p /srv/http/blog/punch-check/mac/' 18 | scp -P 2222 -o StrictHostKeyChecking=no -q punch-check user@delthas.fr:/srv/http/blog/punch-check/mac/punch-check 19 | scp -P 2222 -o StrictHostKeyChecking=no -q punch-check-relay user@delthas.fr:/srv/http/blog/punch-check/mac/punch-check-relay 20 | scp -P 2222 -o StrictHostKeyChecking=no -q punch-check-server user@delthas.fr:/srv/http/blog/punch-check/mac/punch-check-server 21 | -------------------------------------------------------------------------------- /.builds/windows.yml: -------------------------------------------------------------------------------- 1 | image: archlinux 2 | packages: 3 | - git 4 | - go 5 | sources: 6 | - git@github.com:delthas/punch-check.git 7 | secrets: 8 | - 63d7a0ac-2635-4d42-9709-efcd2cf74858 # ssh deploy key 9 | tasks: 10 | - build: | 11 | cd punch-check 12 | GOOS=windows GOARCH=386 go build -ldflags "-s -w" -v -o punch-check.exe ./client 13 | GOOS=windows GOARCH=386 go build -ldflags "-H windowsgui -s -w" -v -o punch-check-gui.exe ./client-gui 14 | GOOS=windows GOARCH=386 go build -ldflags "-s -w" -v -o punch-check-relay.exe ./relay 15 | GOOS=windows GOARCH=386 go build -ldflags "-s -w" -v -o punch-check-server.exe ./server 16 | - deploy: | 17 | cd punch-check 18 | ssh -p 2222 -o StrictHostKeyChecking=no -q user@delthas.fr 'mkdir -p /srv/http/blog/punch-check/windows/' 19 | scp -P 2222 -o StrictHostKeyChecking=no -q punch-check.exe user@delthas.fr:/srv/http/blog/punch-check/windows/punch-check.exe 20 | scp -P 2222 -o StrictHostKeyChecking=no -q punch-check-gui.exe user@delthas.fr:/srv/http/blog/punch-check/windows/punch-check-gui.exe 21 | scp -P 2222 -o StrictHostKeyChecking=no -q punch-check-relay.exe user@delthas.fr:/srv/http/blog/punch-check/windows/punch-check-relay.exe 22 | scp -P 2222 -o StrictHostKeyChecking=no -q punch-check-server.exe user@delthas.fr:/srv/http/blog/punch-check/windows/punch-check-server.exe 23 | -------------------------------------------------------------------------------- /.builds/linux.yml: -------------------------------------------------------------------------------- 1 | image: archlinux 2 | packages: 3 | - git 4 | - go 5 | sources: 6 | - git@github.com:delthas/punch-check.git 7 | secrets: 8 | - 63d7a0ac-2635-4d42-9709-efcd2cf74858 # ssh deploy key 9 | tasks: 10 | - setup: | 11 | # see https://github.com/containers/toolbox/pull/534 12 | curl https://raw.githubusercontent.com/containers/toolbox/master/src/libc-wrappers/libc-wrappers.c -O 13 | gcc -c libc-wrappers.c 14 | ar rcs libc-wrappers.a libc-wrappers.o 15 | - build: | 16 | cd punch-check 17 | go build -ldflags "-s -w -extldflags '-Wl,--wrap,pthread_sigmask $PWD/../libc-wrappers.a' -linkmode external" -v -o punch-check ./client 18 | go build -ldflags "-s -w -extldflags '-Wl,--wrap,pthread_sigmask $PWD/../libc-wrappers.a' -linkmode external" -v -o punch-check-relay ./relay 19 | go build -ldflags "-s -w -extldflags '-Wl,--wrap,pthread_sigmask $PWD/../libc-wrappers.a' -linkmode external" -v -o punch-check-server ./server 20 | - deploy: | 21 | cd punch-check 22 | ssh -p 2222 -o StrictHostKeyChecking=no -q user@delthas.fr 'mkdir -p /srv/http/blog/punch-check/linux/' 23 | ssh -p 2222 -o StrictHostKeyChecking=no -q user@delthas.fr 'rm -f /srv/http/blog/punch-check/linux/punch-check-relay' 24 | ssh -p 2222 -o StrictHostKeyChecking=no -q user@delthas.fr 'rm -f /srv/http/blog/punch-check/linux/punch-check-server' 25 | scp -P 2222 -o StrictHostKeyChecking=no -q punch-check user@delthas.fr:/srv/http/blog/punch-check/linux/punch-check 26 | scp -P 2222 -o StrictHostKeyChecking=no -q punch-check-relay user@delthas.fr:/srv/http/blog/punch-check/linux/punch-check-relay 27 | scp -P 2222 -o StrictHostKeyChecking=no -q punch-check-server user@delthas.fr:/srv/http/blog/punch-check/linux/punch-check-server 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # punch-check [![builds.sr.ht status](https://builds.sr.ht/~delthas/punch-check.svg)](https://builds.sr.ht/~delthas/punch-check?) 2 | 3 | A simple tool to check whether your router supports UDP hole-punching (for example for use with [autopunch](https://github.com/delthas/autopunch)), as well as additional NAT properties. 4 | 5 | ![screenshot](doc/screen.png) 6 | 7 | If you're not sure what to download, choose **GUI Client**. 8 | 9 | | OS | GUI Client | CLI Client | Relay | Server | 10 | | -- | -- | -- | -- | -- | 11 | | Windows | [**Link**](https://delthas.fr/punch-check/windows/punch-check-gui.exe) | [**Link**](https://delthas.fr/punch-check/windows/punch-check.exe) | [**Link**](https://delthas.fr/punch-check/windows/punch-check-relay.exe) | [**Link**](https://delthas.fr/punch-check/windows/punch-check-server.exe) | 12 | | Linux | ✘ | [**Link**](https://delthas.fr/punch-check/linux/punch-check) | [**Link**](https://delthas.fr/punch-check/linux/punch-check-relay) | [**Link**](https://delthas.fr/punch-check/linux/punch-check-server) | 13 | | Mac OS X | ✘ | [**Link**](https://delthas.fr/punch-check/mac/punch-check) | [**Link**](https://delthas.fr/punch-check/mac/punch-check-relay) | [**Link**](https://delthas.fr/punch-check/mac/punch-check-server) | 14 | 15 | ## Design 16 | 17 | [RFC 4787](https://tools.ietf.org/html/rfc4787) defines several NAT properties and which are needed for hole-punching support. Those properties which are checked by this tool are: 18 | - port mapping: either endpoint-independent, address-dependent, or address and port-dependent 19 | - filtering: either endpoint-independent, address-dependent, or address and port-dependent 20 | - hairpinning: supported or unsupported 21 | - port assignment: may be contiguous, preserving, and parity-preserving 22 | 23 | For checking these properties, the client would need to connect to at least 2 different IPs, and at least 2 differents ports on an IP. It would also need to create several mappings from several local ports. 24 | 25 | The following topology is therefore used: 26 | - a central server sends "packet send" requests to relays and clients, and gathers "packet received" responses from them, then sends the final test results to the client when done 27 | - several relays connect to this central server with a control TCP socket, and open UDP sockets on publicly accessible ports, then wait for requests from the server 28 | - several clients connect to the server with a control TCP socket, open UDP sockets behind their NAT, then wait for requests from the server 29 | 30 | The server sends specific "packet send" requests to relays and clients in order to detect all properties of the client NAT as fast as possible with minimal state. 31 | 32 | Let C be the client machine; A and B two relays; and let Xi be the i-th port of machine X. The packet send requests are: 33 | 34 | From the same local port, send to two different IPs and two differents ports of an IP to check port mapping behaviour. 35 | - C0 -> A0 36 | - C0 -> A1 37 | - C0 -> B0 38 | 39 | Try to send packets from two different IPs and two different ports of an IP; one of which is the destination of the client mapping, to check filtering behaviour. 40 | - C1 -> A0 41 | - A0 -> C1 42 | - A1 -> C1 43 | - B0 -> C1 44 | 45 | Try to send packets from several local ports to the same IP and port to check port assignment behaviour. 46 | - C2 -> A0 47 | - C3 -> A0 48 | - C4 -> A0 49 | 50 | Try to send packets from two local ports to their local NAT ports to check hairpinning behaviour. 51 | - C1 -> C2 52 | - C2 -> C1 53 | 54 | All these requests are done simultaneously (provided the needed NAT ports are known). The properties of the NAT are derived from which packets were received and what NAT ports were used for the mappings. 55 | 56 | ## Acknowledgements 57 | 58 | - [RFC4787](https://tools.ietf.org/html/rfc4787) 59 | - [draft-jennings-behave-test-results](https://tools.ietf.org/html/draft-jennings-behave-test-results-01) 60 | - [Peer-to-Peer Communication Across Network Address Translators](https://bford.info/pub/net/p2pnat/) 61 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "os" 10 | "sync" 11 | "sync/atomic" 12 | 13 | . "github.com/delthas/punch-check" 14 | ) 15 | 16 | var defaultServerHost = "delthas.fr" 17 | var defaultStartPort = 34500 18 | 19 | var logErr = log.New(os.Stderr, "", 0) 20 | var logDebug *log.Logger 21 | 22 | var closed uint32 = 0 23 | 24 | func main() { 25 | serverHost := flag.String("host", defaultServerHost, "server hostname[:port]") 26 | debug := flag.Bool("debug", false, "add debug logging") 27 | flag.Parse() 28 | 29 | if *debug { 30 | logDebug = log.New(os.Stderr, "debug: ", log.Ldate|log.Ltime|log.Lshortfile) 31 | } else { 32 | logDebug = log.New(ioutil.Discard, "", 0) 33 | } 34 | 35 | var serverAddr *net.TCPAddr 36 | _, _, err := net.SplitHostPort(*serverHost) 37 | if err != nil { 38 | serverAddr, err = ResolveTCPBySRV("punchcheck", *serverHost) 39 | if err != nil { 40 | logErr.Fatalf("failed resolving server host %q: %v", *serverHost, err) 41 | } 42 | } else { 43 | serverAddr, err = net.ResolveTCPAddr("tcp4", *serverHost) 44 | if err != nil { 45 | logErr.Fatalf("failed resolving server host %q: %v", *serverHost, err) 46 | } 47 | } 48 | 49 | defer atomic.StoreUint32(&closed, 1) 50 | 51 | cs := make([]*net.UDPConn, 10) 52 | var startPort int 53 | outer: 54 | for startPort = defaultStartPort; ; startPort += len(cs) { 55 | for i := range cs { 56 | c, err := net.ListenUDP("udp4", &net.UDPAddr{ 57 | Port: startPort + i, 58 | }) 59 | if err != nil { 60 | if startPort > defaultStartPort+len(cs)*10 { 61 | log.Fatalf("failed creating UDP sockets after 10 tries: %v", err) 62 | } 63 | continue outer 64 | } 65 | cs[i] = c 66 | } 67 | break 68 | } 69 | ports := make([]int, len(cs)) 70 | for i := range ports { 71 | ports[i] = startPort + i 72 | } 73 | 74 | control, err := net.DialTCP("tcp4", nil, serverAddr) 75 | if err != nil { 76 | logErr.Fatalf("failed dialing server at %q: %v", *serverHost, err) 77 | } 78 | control.SetNoDelay(true) 79 | defer control.Close() 80 | logDebug.Printf("connected to server: %q", *serverHost) 81 | 82 | WriteMessage(control, &MessagePorts{ 83 | Ports: ports, 84 | }) 85 | 86 | var writeLock sync.Mutex 87 | for i, c := range cs { 88 | c := c 89 | i := i 90 | go func() { 91 | for { 92 | buf := make([]byte, 1536) 93 | n, addr, err := c.ReadFromUDP(buf) 94 | if err != nil { 95 | if atomic.LoadUint32(&closed) == 1 { 96 | return 97 | } 98 | logErr.Fatalf("reading from UDP socket: %v", err) 99 | } 100 | buf = buf[:n] 101 | 102 | logDebug.Printf("forwarding read from %s:%d on %d: %v", addr.IP.String(), addr.Port, ports[i], buf) 103 | writeLock.Lock() 104 | WriteMessage(control, &MessageReceive{ 105 | LocalPort: ports[i], 106 | IP: addr.IP, 107 | Port: addr.Port, 108 | Data: buf, 109 | }) 110 | writeLock.Unlock() 111 | } 112 | }() 113 | } 114 | 115 | for { 116 | m, err := ReadMessage(control) 117 | if err != nil { 118 | logErr.Fatalf("reading message from control socket: %v", err) 119 | return 120 | } 121 | switch m := m.(type) { 122 | case *MessageSend: 123 | if m.LocalPort < startPort || m.LocalPort >= startPort+len(ports) { 124 | logErr.Fatalf("invalid send message: invalid local port: %d", m.LocalPort) 125 | } 126 | logDebug.Printf("writing to %s:%d from %d: %v", net.IP(m.IP).String(), m.Port, m.LocalPort, m.Data) 127 | cs[m.LocalPort-startPort].WriteToUDP(m.Data, &net.UDPAddr{ 128 | IP: m.IP, 129 | Port: m.Port, 130 | }) 131 | case *MessageInfo: 132 | if m.MessageType == 0 { 133 | logErr.Fatalf("error: %s", m.Message) 134 | } else if m.MessageType != 1 { 135 | logErr.Fatalf("message of unknown message type %d: %s", m.MessageType, m.Message) 136 | } 137 | fmt.Println(m.Message) 138 | return 139 | default: 140 | logErr.Fatalf("invalid message type: %v", MessageType(m.Type())) 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /client-gui/client.go: -------------------------------------------------------------------------------- 1 | //go:generate rsrc -arch=amd64 -manifest client.manifest -o rsrc_amd64.syso 2 | //go:generate rsrc -arch=386 -manifest client.manifest -o rsrc_386.syso 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "os" 10 | "strings" 11 | "sync" 12 | 13 | "github.com/lxn/walk" 14 | . "github.com/lxn/walk/declarative" 15 | 16 | . "github.com/delthas/punch-check" 17 | ) 18 | 19 | var serverHost = "delthas.fr" 20 | var defaultStartPort = 34500 21 | 22 | var mw *walk.MainWindow 23 | var text *walk.TextEdit 24 | var progress *walk.ProgressBar 25 | 26 | func log(f string, args ...interface{}) { 27 | mw.Synchronize(func() { 28 | text.AppendText(strings.ReplaceAll(fmt.Sprintf(f+"\n", args...), "\n", "\r\n")) 29 | }) 30 | } 31 | 32 | func process() { 33 | serverAddr, err := ResolveTCPBySRV("punchcheck", serverHost) 34 | if err != nil { 35 | log("failed resolving server host %q: %v", serverHost, err) 36 | } 37 | 38 | cs := make([]*net.UDPConn, 10) 39 | var startPort int 40 | outer: 41 | for startPort = defaultStartPort; ; startPort += len(cs) { 42 | for i := range cs { 43 | c, err := net.ListenUDP("udp4", &net.UDPAddr{ 44 | Port: startPort + i, 45 | }) 46 | if err != nil { 47 | if startPort > defaultStartPort+len(cs)*10 { 48 | log("failed creating UDP sockets after 10 tries: %v", err) 49 | return 50 | } 51 | continue outer 52 | } 53 | cs[i] = c 54 | } 55 | break 56 | } 57 | ports := make([]int, len(cs)) 58 | for i := range ports { 59 | ports[i] = startPort + i 60 | } 61 | 62 | control, err := net.DialTCP("tcp4", nil, serverAddr) 63 | if err != nil { 64 | log("failed dialing server at %q: %v", serverHost, err) 65 | return 66 | } 67 | control.SetNoDelay(true) 68 | defer control.Close() 69 | 70 | WriteMessage(control, &MessagePorts{ 71 | Ports: ports, 72 | }) 73 | 74 | var writeLock sync.Mutex 75 | for i, c := range cs { 76 | c := c 77 | i := i 78 | go func() { 79 | for { 80 | buf := make([]byte, 1536) 81 | n, addr, err := c.ReadFromUDP(buf) 82 | if err != nil { 83 | log("reading from UDP socket: %v", err) 84 | return 85 | } 86 | buf = buf[:n] 87 | 88 | writeLock.Lock() 89 | WriteMessage(control, &MessageReceive{ 90 | LocalPort: ports[i], 91 | IP: addr.IP, 92 | Port: addr.Port, 93 | Data: buf, 94 | }) 95 | writeLock.Unlock() 96 | } 97 | }() 98 | } 99 | 100 | for { 101 | m, err := ReadMessage(control) 102 | if err != nil { 103 | log("reading message from control socket: %v", err) 104 | return 105 | } 106 | switch m := m.(type) { 107 | case *MessageSend: 108 | if m.LocalPort < startPort || m.LocalPort >= startPort+len(ports) { 109 | log("invalid send message: invalid local port: %d", m.LocalPort) 110 | return 111 | } 112 | cs[m.LocalPort-startPort].WriteToUDP(m.Data, &net.UDPAddr{ 113 | IP: m.IP, 114 | Port: m.Port, 115 | }) 116 | case *MessageInfo: 117 | if m.MessageType == 0 { 118 | log("error: %s", m.Message) 119 | } else if m.MessageType != 1 { 120 | log("message of unknown message type %d: %s", m.MessageType, m.Message) 121 | } else { 122 | log("%s", m.Message) 123 | } 124 | return 125 | default: 126 | log("invalid message type: %v", MessageType(m.Type())) 127 | return 128 | } 129 | } 130 | } 131 | 132 | func main() { 133 | size := Size{Width: 400, Height: 250} 134 | err := MainWindow{ 135 | AssignTo: &mw, 136 | Title: "PunchCheck", 137 | MinSize: size, 138 | Size: size, 139 | Layout: VBox{}, 140 | Children: []Widget{ 141 | TextEdit{ 142 | AssignTo: &text, 143 | ReadOnly: true, 144 | }, 145 | ProgressBar{ 146 | AssignTo: &progress, 147 | MarqueeMode: true, 148 | Value: 1, 149 | }, 150 | }, 151 | }.Create() 152 | if err != nil { 153 | panic(err) 154 | } 155 | 156 | go func() { 157 | process() 158 | mw.Synchronize(func() { 159 | progress.SetMarqueeMode(false) 160 | progress.SetValue(100) 161 | }) 162 | }() 163 | 164 | code := mw.Run() 165 | os.Exit(code) 166 | } 167 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package punch 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net" 10 | "strconv" 11 | ) 12 | 13 | type MessageType byte 14 | 15 | var ( 16 | SendType MessageType = 0 17 | ReceiveType MessageType = 1 18 | InfoType MessageType = 2 19 | PortsType MessageType = 3 20 | PingType MessageType = 4 21 | ) 22 | 23 | type Message interface { 24 | Type() MessageType 25 | } 26 | 27 | type MessageInfo struct { 28 | MessageType int `json:"message_type"` 29 | Message string `json:"message"` 30 | } 31 | 32 | func (m *MessageInfo) Type() MessageType { 33 | return InfoType 34 | } 35 | 36 | type MessagePorts struct { 37 | Ports []int `json:"ports"` 38 | } 39 | 40 | func (m *MessagePorts) Type() MessageType { 41 | return PortsType 42 | } 43 | 44 | type MessageSend struct { 45 | LocalPort int `json:"local_port"` 46 | IP []byte `json:"ip"` 47 | Port int `json:"port"` 48 | Data []byte `json:"data"` 49 | } 50 | 51 | func (m *MessageSend) Type() MessageType { 52 | return SendType 53 | } 54 | 55 | type MessageReceive struct { 56 | LocalPort int `json:"local_port"` 57 | IP []byte `json:"ip"` 58 | Port int `json:"port"` 59 | Data []byte `json:"data"` 60 | } 61 | 62 | func (m *MessageReceive) Type() MessageType { 63 | return ReceiveType 64 | } 65 | 66 | type MessagePing struct { 67 | } 68 | 69 | func (m *MessagePing) Type() MessageType { 70 | return PingType 71 | } 72 | 73 | func ReadMessage(c *net.TCPConn) (Message, error) { 74 | header := make([]byte, 3) 75 | _, err := io.ReadFull(c, header) 76 | if err != nil { 77 | return nil, fmt.Errorf("reading message header: read error: %v", err) 78 | } 79 | mt := header[0] 80 | ml := int64(binary.BigEndian.Uint16(header[1:])) 81 | var m Message 82 | switch MessageType(mt) { 83 | case SendType: 84 | m = &MessageSend{} 85 | case ReceiveType: 86 | m = &MessageReceive{} 87 | case InfoType: 88 | m = &MessageInfo{} 89 | case PortsType: 90 | m = &MessagePorts{} 91 | case PingType: 92 | m = &MessagePing{} 93 | default: 94 | return nil, fmt.Errorf("reading message: unknown message type: %v", MessageType(mt)) 95 | } 96 | if ml == 0 { 97 | return m, nil 98 | } 99 | jr := io.LimitReader(c, ml) 100 | jd := json.NewDecoder(jr) 101 | if err := jd.Decode(&m); err != nil { 102 | return nil, fmt.Errorf("reading message: parsing message: %v", err) 103 | } 104 | if _, err := io.Copy(ioutil.Discard, jr); err != nil { 105 | return nil, fmt.Errorf("reading message: seeking to end of json payload: %v", err) 106 | } 107 | return m, nil 108 | } 109 | 110 | func WriteMessage(c *net.TCPConn, m Message) error { 111 | header := make([]byte, 3) 112 | header[0] = byte(m.Type()) 113 | data, err := json.Marshal(m) 114 | if err != nil { 115 | return fmt.Errorf("writing message: marshaling error: %v", err) 116 | } 117 | binary.BigEndian.PutUint16(header[1:], uint16(len(data))) 118 | if _, err := c.Write(header); err != nil { 119 | return fmt.Errorf("writing message header: write error: %v", err) 120 | } 121 | if _, err := c.Write(data); err != nil { 122 | return fmt.Errorf("writing message data: write error: %v", err) 123 | } 124 | return nil 125 | } 126 | 127 | func Index(a []int, e int) int { 128 | for i, v := range a { 129 | if v == e { 130 | return i 131 | } 132 | } 133 | return -1 134 | } 135 | 136 | func ResolveTCPBySRV(service string, host string) (*net.TCPAddr, error) { 137 | _, srvs, err := net.LookupSRV(service, "tcp", host) 138 | if err != nil { 139 | return nil, fmt.Errorf("resolving service %q of host %q: %v", service, host, err) 140 | } 141 | if len(srvs) == 0 { 142 | return nil, fmt.Errorf("resolving service %q of host %q: no SRV records found", service, host) 143 | } 144 | var lastRecord string 145 | for _, srv := range srvs { 146 | var addr *net.TCPAddr 147 | addr, err = net.ResolveTCPAddr("tcp4", net.JoinHostPort(srv.Target, strconv.Itoa(int(srv.Port)))) 148 | if err != nil { 149 | lastRecord = srv.Target 150 | continue 151 | } 152 | return addr, nil 153 | } 154 | return nil, fmt.Errorf("resolving service %q of host %q: resolving %q: %v", service, host, lastRecord, err) 155 | } 156 | 157 | type StringSliceFlag []string 158 | 159 | func (v *StringSliceFlag) String() string { 160 | return fmt.Sprint([]string(*v)) 161 | } 162 | 163 | func (v *StringSliceFlag) Set(s string) error { 164 | *v = append(*v, s) 165 | return nil 166 | } 167 | 168 | var ClientRelaysCount = 2 169 | var ClientPortsCount = 5 170 | var RelayPortsCount = 2 171 | -------------------------------------------------------------------------------- /relay/relay.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net" 9 | "os" 10 | "strconv" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | 15 | . "github.com/delthas/punch-check" 16 | ) 17 | 18 | var retryTimeout = 15 * time.Second 19 | 20 | var logErr = log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lshortfile) 21 | var logDebug *log.Logger 22 | 23 | var mutex sync.Mutex 24 | var control *net.TCPConn 25 | 26 | var cs []*net.UDPConn 27 | var ports []int 28 | 29 | var closed uint32 = 0 30 | 31 | func writeControl(m Message) { 32 | mutex.Lock() 33 | defer mutex.Unlock() 34 | if control == nil { 35 | return 36 | } 37 | WriteMessage(control, m) 38 | } 39 | 40 | func main() { 41 | serverHost := flag.String("host", "", "server hostname[:port] (required)") 42 | debug := flag.Bool("debug", false, "add debug logging") 43 | var portsStr []string 44 | flag.Var((*StringSliceFlag)(&portsStr), "port", "port to listen on (pass multiple times for multiple ports)") 45 | flag.Parse() 46 | 47 | if len(portsStr) < RelayPortsCount { 48 | fmt.Fprintf(os.Stderr, "at least %d ports are required (use -port)\n", RelayPortsCount) 49 | flag.Usage() 50 | return 51 | } 52 | if *serverHost == "" { 53 | fmt.Fprintf(os.Stderr, "-host is required\n") 54 | flag.Usage() 55 | return 56 | } 57 | 58 | if *debug { 59 | logDebug = log.New(os.Stderr, "debug: ", log.Ldate|log.Ltime|log.Lshortfile) 60 | } else { 61 | logDebug = log.New(ioutil.Discard, "", 0) 62 | } 63 | 64 | var serverAddr *net.TCPAddr 65 | _, _, err := net.SplitHostPort(*serverHost) 66 | if err != nil { 67 | serverAddr, err = ResolveTCPBySRV("punchcheck", *serverHost) 68 | if err != nil { 69 | logErr.Fatalf("failed resolving server host %q: %v", *serverHost, err) 70 | } 71 | } else { 72 | serverAddr, err = net.ResolveTCPAddr("tcp4", *serverHost) 73 | if err != nil { 74 | logErr.Fatalf("failed resolving server host %q: %v", *serverHost, err) 75 | } 76 | } 77 | 78 | defer atomic.StoreUint32(&closed, 1) 79 | 80 | cs = make([]*net.UDPConn, len(portsStr)) 81 | ports = make([]int, len(cs)) 82 | for i, portStr := range portsStr { 83 | port, err := strconv.Atoi(portStr) 84 | if err != nil { 85 | log.Fatalf("failed parsing UDP port %d: %v", port, err) 86 | } 87 | c, err := net.ListenUDP("udp4", &net.UDPAddr{ 88 | Port: port, 89 | }) 90 | if err != nil { 91 | log.Fatalf("failed creating UDP socket for port %d: %v", port, err) 92 | } 93 | cs[i] = c 94 | ports[i] = port 95 | } 96 | 97 | for i, c := range cs { 98 | c := c 99 | i := i 100 | go func() { 101 | for { 102 | buf := make([]byte, 1536) 103 | n, addr, err := c.ReadFromUDP(buf) 104 | if err != nil { 105 | if atomic.LoadUint32(&closed) == 1 { 106 | return 107 | } 108 | logErr.Fatalf("reading from UDP socket: %v", err) 109 | } 110 | buf = buf[:n] 111 | 112 | logDebug.Printf("forwarding read from %s:%d on %d: %v", addr.IP.String(), addr.Port, ports[i], buf) 113 | writeControl(&MessageReceive{ 114 | LocalPort: ports[i], 115 | IP: addr.IP, 116 | Port: addr.Port, 117 | Data: buf, 118 | }) 119 | } 120 | }() 121 | } 122 | 123 | first := true 124 | for { 125 | if !first { 126 | time.Sleep(15 * time.Second) 127 | } else { 128 | first = false 129 | } 130 | c, err := net.DialTCP("tcp4", nil, serverAddr) 131 | if err != nil { 132 | logErr.Printf("failed dialing server at %q, retrying in %v: %v", *serverHost, retryTimeout, err) 133 | continue 134 | } 135 | c.SetNoDelay(true) 136 | logErr.Printf("connected to server: %q", *serverHost) 137 | WriteMessage(c, &MessagePorts{ 138 | Ports: ports, 139 | }) 140 | mutex.Lock() 141 | control = c 142 | mutex.Unlock() 143 | 144 | outer: 145 | for { 146 | m, err := ReadMessage(control) 147 | if err != nil { 148 | logErr.Printf("reading message from control socket: %v", err) 149 | break 150 | } 151 | switch m := m.(type) { 152 | case *MessageSend: 153 | index := Index(ports, m.LocalPort) 154 | if index == -1 { 155 | logErr.Fatalf("invalid send message: invalid local port: %d", m.LocalPort) 156 | } 157 | logDebug.Printf("writing to %s:%d from %d: %v", net.IP(m.IP).String(), m.Port, m.LocalPort, m.Data) 158 | cs[index].WriteToUDP(m.Data, &net.UDPAddr{ 159 | IP: m.IP, 160 | Port: m.Port, 161 | }) 162 | default: 163 | logErr.Printf("invalid message type: %v", MessageType(m.Type())) 164 | break outer 165 | } 166 | } 167 | 168 | logErr.Printf("disconnected from server, retrying in %v", retryTimeout) 169 | mutex.Lock() 170 | c.Close() 171 | control = nil 172 | mutex.Unlock() 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "math/rand" 9 | "net" 10 | "os" 11 | "time" 12 | 13 | . "github.com/delthas/punch-check" 14 | ) 15 | 16 | type event interface{} 17 | 18 | type eventNew struct { 19 | c *net.TCPConn 20 | w chan Message 21 | } 22 | 23 | type eventClosed struct { 24 | c *net.TCPConn 25 | err error 26 | } 27 | 28 | type eventRead struct { 29 | c *net.TCPConn 30 | message Message 31 | } 32 | 33 | type connection struct { 34 | addr *net.TCPAddr 35 | c *net.TCPConn 36 | w chan Message 37 | ports []int 38 | client *client // nil if connection is a relay 39 | } 40 | 41 | func (c *connection) Write(localPort int, ip net.IP, port int) { 42 | data := make([]byte, 2) 43 | binary.BigEndian.PutUint16(data, uint16(localPort)) 44 | c.w <- &MessageSend{ 45 | LocalPort: localPort, 46 | IP: ip, 47 | Port: port, 48 | Data: data, 49 | } 50 | } 51 | 52 | type client struct { 53 | last time.Time 54 | relays []*connection 55 | natPorts []int 56 | natPortDependentPort int 57 | natEndpointDependentPort int 58 | received bool 59 | receivedPortDependent bool 60 | receivedEndpointDependent bool 61 | receivedHairpinning bool 62 | } 63 | 64 | func (c *client) Done() bool { 65 | for _, port := range c.natPorts { 66 | if port == 0 { 67 | return false 68 | } 69 | } 70 | if c.natPortDependentPort == 0 || c.natEndpointDependentPort == 0 { 71 | return false 72 | } 73 | if !c.received || !c.receivedPortDependent || !c.receivedEndpointDependent || !c.receivedHairpinning { 74 | return false 75 | } 76 | return true 77 | } 78 | 79 | var punchTimeout = 5 * time.Second 80 | 81 | var logErr = log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lshortfile) 82 | var logDebug = log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lshortfile) 83 | 84 | var allowedRelays []net.IP 85 | 86 | var defaultServerPort = 17485 87 | 88 | var events = make(chan event, 1000) 89 | var connections = make(map[*net.TCPConn]*connection) 90 | 91 | func acceptConnections(l *net.TCPListener) { 92 | for { 93 | c, err := l.AcceptTCP() 94 | if err != nil { 95 | break 96 | } 97 | c.SetNoDelay(true) 98 | w := make(chan Message) 99 | events <- eventNew{ 100 | c: c, 101 | w: w, 102 | } 103 | go func() { 104 | for { 105 | m, err := ReadMessage(c) 106 | if err != nil { 107 | events <- eventClosed{ 108 | c: c, 109 | err: err, 110 | } 111 | return 112 | } 113 | events <- eventRead{ 114 | c: c, 115 | message: m, 116 | } 117 | } 118 | }() 119 | go func() { 120 | for m := range w { 121 | WriteMessage(c, m) 122 | } 123 | c.Close() 124 | }() 125 | } 126 | } 127 | 128 | func isRelay(addr *net.TCPAddr) bool { 129 | for _, relay := range allowedRelays { 130 | if relay.Equal(addr.IP) { 131 | return true 132 | } 133 | } 134 | return false 135 | } 136 | 137 | func main() { 138 | serverPort := flag.Int("port", defaultServerPort, "port to listen on") 139 | var allowedRelayHosts []string 140 | flag.Var((*StringSliceFlag)(&allowedRelayHosts), "relay", "relay hostname/ip (pass multiple times for multiple relays)") 141 | flag.Parse() 142 | 143 | if len(allowedRelayHosts) < ClientRelaysCount { 144 | fmt.Fprintf(os.Stderr, "at least %d relays are required (use -relay)\n", ClientRelaysCount) 145 | flag.Usage() 146 | return 147 | } 148 | 149 | allowedRelays = make([]net.IP, len(allowedRelayHosts)) 150 | for i, relayHost := range allowedRelayHosts { 151 | addr, err := net.ResolveIPAddr("ip4", relayHost) 152 | if err != nil { 153 | logErr.Fatalf("failed resolving relay host %q: %v", relayHost, err) 154 | } 155 | allowedRelays[i] = addr.IP 156 | } 157 | 158 | l, err := net.ListenTCP("tcp4", &net.TCPAddr{ 159 | Port: *serverPort, 160 | }) 161 | if err != nil { 162 | logErr.Fatalf("failed creating control server socket on port %d: %v", *serverPort, err) 163 | } 164 | go acceptConnections(l) 165 | 166 | process() 167 | } 168 | 169 | func process() { 170 | for { 171 | select { 172 | case event := <-events: 173 | switch e := event.(type) { 174 | case eventNew: 175 | if _, ok := connections[e.c]; ok { 176 | break 177 | } 178 | addr := e.c.RemoteAddr().(*net.TCPAddr) 179 | var data *client 180 | if isRelay(addr) { 181 | for _, relay := range connections { 182 | if relay.client != nil { 183 | continue 184 | } 185 | if relay.addr.IP.Equal(addr.IP) { 186 | logErr.Printf("received new event of relayed that is already connected: %q", addr.IP.String()) 187 | e.w <- &MessageInfo{ 188 | MessageType: 0, 189 | Message: "Internal error: Relay is already connected.", 190 | } 191 | close(e.w) 192 | } 193 | } 194 | } else { 195 | relayCount := 0 196 | for _, relay := range connections { 197 | if relay.client != nil { 198 | continue 199 | } 200 | relayCount++ 201 | } 202 | if relayCount < ClientRelaysCount { 203 | logErr.Printf("not enough relays for client connection: want %d, has %d", ClientRelaysCount, relayCount) 204 | e.w <- &MessageInfo{ 205 | MessageType: 0, 206 | Message: "Internal error: Not enough relays available.", 207 | } 208 | close(e.w) 209 | break 210 | } 211 | relayIndexes := make(map[int]struct{}, ClientRelaysCount) 212 | for i := 0; i < ClientRelaysCount; i++ { 213 | relay := rand.Intn(relayCount) 214 | for { 215 | if _, ok := relayIndexes[relay]; !ok { 216 | break 217 | } 218 | relay = rand.Intn(relayCount) 219 | } 220 | relayIndexes[relay] = struct{}{} 221 | } 222 | relays := make([]*connection, ClientRelaysCount) 223 | i := 0 224 | ri := 0 225 | for _, relay := range connections { 226 | if relay.client != nil { 227 | continue 228 | } 229 | if _, ok := relayIndexes[ri]; ok { 230 | relays[i] = relay 231 | i++ 232 | } 233 | ri++ 234 | } 235 | data = &client{ 236 | last: time.Now(), 237 | relays: relays, 238 | natPorts: make([]int, ClientPortsCount), 239 | } 240 | } 241 | connections[e.c] = &connection{ 242 | addr: addr, 243 | c: e.c, 244 | w: e.w, 245 | client: data, 246 | } 247 | case eventClosed: 248 | if _, ok := connections[e.c]; ok && e.err != nil { 249 | logErr.Printf("connection closed: %v", e.err) 250 | } 251 | closeConnection(e.c, nil) 252 | case eventRead: 253 | c, ok := connections[e.c] 254 | if !ok { 255 | break 256 | } 257 | switch m := e.message.(type) { 258 | case *MessagePing: 259 | closeConnection(e.c, &MessageInfo{ 260 | MessageType: 1, 261 | Message: "OK", 262 | }) 263 | case *MessagePorts: 264 | if c.ports != nil { 265 | logErr.Printf("received duplicate ports message: %v", e.message.Type()) 266 | closeConnection(e.c, &MessageInfo{ 267 | MessageType: 0, 268 | Message: "Internal error: Unexpected ports message.", 269 | }) 270 | break 271 | } 272 | var minPorts int 273 | if c.client != nil { 274 | minPorts = ClientPortsCount 275 | } else { 276 | minPorts = RelayPortsCount 277 | } 278 | if len(m.Ports) < minPorts { 279 | logErr.Printf("received invalid ports message: not enough ports: want %d, got %d", minPorts, len(m.Ports)) 280 | closeConnection(e.c, &MessageInfo{ 281 | MessageType: 0, 282 | Message: "Internal error: Invalid ports message: not enough ports.", 283 | }) 284 | break 285 | } 286 | if !unique(m.Ports) { 287 | logErr.Printf("received invalid ports message: ports are not unique: %v", m.Ports) 288 | closeConnection(e.c, &MessageInfo{ 289 | MessageType: 0, 290 | Message: "Internal error: Invalid ports message: ports are not unique.", 291 | }) 292 | break 293 | } 294 | c.ports = m.Ports[:minPorts] 295 | case *MessageReceive: 296 | if len(m.Data) != 2 { 297 | break 298 | } 299 | remotePort := int(binary.BigEndian.Uint16(m.Data)) 300 | var client *connection 301 | var relay *connection 302 | var clientPort int 303 | var clientNatPort int 304 | var relayPort int 305 | 306 | if c.client == nil { 307 | relay = c 308 | for _, c := range connections { 309 | if c.client != nil && c.addr.IP.Equal(m.IP) { 310 | client = c 311 | break 312 | } 313 | } 314 | if client == nil { 315 | logErr.Printf("received invalid receive message: unknown client: %s", net.IP(m.IP).String()) 316 | break 317 | } 318 | clientPort = remotePort 319 | clientNatPort = m.Port 320 | relayPort = m.LocalPort 321 | } else { 322 | client = c 323 | if client.addr.IP.Equal(m.IP) { 324 | if m.LocalPort == client.ports[1] && m.Port == client.client.natPorts[2] { 325 | client.client.receivedHairpinning = true 326 | } 327 | break 328 | } 329 | for _, r := range c.client.relays { 330 | if r.addr.IP.Equal(m.IP) { 331 | relay = r 332 | break 333 | } 334 | } 335 | if relay == nil { 336 | logErr.Printf("received invalid receive message: unknown relay: %s", net.IP(m.IP).String()) 337 | break 338 | } 339 | clientPort = m.LocalPort 340 | relayPort = m.Port 341 | } 342 | 343 | relayIndex := -1 // 0, 0 344 | for i, r := range client.client.relays { 345 | if r == relay { 346 | relayIndex = i 347 | break 348 | } 349 | } 350 | if relayIndex == -1 { 351 | logErr.Printf("received invalid receive message: unknown relay: %v", net.IP(relay.addr.IP)) 352 | break 353 | } 354 | 355 | clientPortIndex := Index(client.ports, clientPort) // C<0>, C<1> 356 | if clientPortIndex == -1 { 357 | logErr.Printf("received invalid receive message: unknown client port: %v:%d", net.IP(client.addr.IP), clientPort) 358 | break 359 | } 360 | relayPortIndex := Index(relay.ports, relayPort) // A<0>, A<1> 361 | if relayPortIndex == -1 { 362 | logErr.Printf("received invalid receive message: unknown relay port for relay %v: %d", net.IP(relay.addr.IP), relayPort) 363 | break 364 | } 365 | 366 | if c.client == nil { 367 | if relayIndex == 0 && relayPortIndex == 0 { // C* -> A0 368 | client.client.natPorts[clientPortIndex] = clientNatPort 369 | } else if clientPortIndex == 0 { 370 | if relayIndex == 0 && relayPortIndex == 1 { // C0 -> A1 371 | client.client.natPortDependentPort = clientNatPort 372 | } else if relayIndex == 1 && relayPortIndex == 0 { // C0 -> B0 373 | client.client.natEndpointDependentPort = clientNatPort 374 | } 375 | } 376 | } else { 377 | if clientPortIndex == 1 { 378 | if relayIndex == 0 && relayPortIndex == 0 { // A0 -> C1 379 | client.client.received = true 380 | } else if relayIndex == 0 && relayPortIndex == 1 { // A1 -> C1 381 | client.client.receivedPortDependent = true 382 | } else if relayIndex == 1 && relayPortIndex == 0 { // B0 -> C1 383 | client.client.receivedEndpointDependent = true 384 | } 385 | } 386 | } 387 | default: 388 | logErr.Printf("received unexpected message type: %v", MessageType(e.message.Type())) 389 | closeConnection(e.c, &MessageInfo{ 390 | MessageType: 0, 391 | Message: fmt.Sprintf("Internal error: Invalid message type: %v.", MessageType(e.message.Type())), 392 | }) 393 | break 394 | } 395 | } 396 | case <-time.After(50 * time.Millisecond): 397 | now := time.Now() 398 | for key, client := range connections { 399 | if client.client == nil { 400 | continue 401 | } 402 | if now.Sub(client.client.last) > punchTimeout || client.client.Done() { 403 | var message string 404 | if !client.client.received || client.client.natPorts[0] == 0 { 405 | message = "Test failed. UDP is blocked." 406 | } else { 407 | message = "Test complete.\n" 408 | var filtering string 409 | if client.client.receivedEndpointDependent { 410 | filtering = "endpoint-independent" 411 | } else if client.client.receivedPortDependent { 412 | filtering = "address-dependent" 413 | } else { 414 | filtering = "address and port-dependent" 415 | } 416 | holepunching := false 417 | var mapping string 418 | if client.client.natEndpointDependentPort == client.client.natPorts[0] { 419 | holepunching = true 420 | mapping = "endpoint-independent" 421 | } else if client.client.natPortDependentPort == client.client.natPorts[0] { 422 | mapping = "address-dependent" 423 | } else { 424 | mapping = "address and port-dependent" 425 | } 426 | parity := true 427 | preserved := true 428 | contiguous := true 429 | last := 0 430 | for i, port := range client.ports { 431 | natPort := client.client.natPorts[i] 432 | if natPort == 0 { 433 | last = 0 434 | continue 435 | } 436 | if port%2 != natPort%2 { 437 | parity = false 438 | } 439 | if port != natPort { 440 | preserved = false 441 | } 442 | if last != 0 && last != natPort+1 { 443 | contiguous = false 444 | } 445 | last = natPort 446 | } 447 | if holepunching { 448 | message += "Hole-punching is supported.\n" 449 | } else { 450 | message += "Hole-punching is NOT supported.\n" 451 | } 452 | message += fmt.Sprintf("Filtering: %s.\nMapping: %s.\n", filtering, mapping) 453 | if client.client.receivedHairpinning { 454 | message += "Hairpinning is supported.\n" 455 | } 456 | if parity { 457 | message += "Assignment preserves parity.\n" 458 | } 459 | if preserved { 460 | message += "Assignment preserves local port.\n" 461 | } 462 | if contiguous { 463 | message += "Assignment preserves contiguity.\n" 464 | } 465 | } 466 | closeConnection(key, &MessageInfo{ 467 | MessageType: 1, 468 | Message: message, 469 | }) 470 | continue 471 | } 472 | if client.ports == nil { 473 | continue 474 | } 475 | for i := len(client.ports) - 1; i >= 0; i-- { // C* -> A0 476 | // send in reverse order to check both assignment contiguity and preservation 477 | clientPort := client.ports[i] 478 | relay := client.client.relays[0] 479 | client.Write(clientPort, relay.addr.IP, relay.ports[0]) 480 | } 481 | { // C0 -> A1 482 | relay := client.client.relays[0] 483 | client.Write(client.ports[0], relay.addr.IP, relay.ports[1]) 484 | } 485 | { // C0 -> B0 486 | relay := client.client.relays[1] 487 | client.Write(client.ports[0], relay.addr.IP, relay.ports[0]) 488 | } 489 | natPort := client.client.natPorts[1] 490 | if natPort != 0 { 491 | { 492 | relay := client.client.relays[0] 493 | relay.Write(relay.ports[0], client.addr.IP, natPort) // A0 -> C1 494 | relay.Write(relay.ports[1], client.addr.IP, natPort) // A1 -> C1 495 | } 496 | { 497 | relay := client.client.relays[1] 498 | relay.Write(relay.ports[0], client.addr.IP, natPort) // B0 -> C1 499 | } 500 | } 501 | if client.client.natPorts[1] != 0 && client.client.natPorts[2] != 0 { 502 | client.Write(client.ports[1], client.addr.IP, client.client.natPorts[2]) // C1 -> C2 503 | client.Write(client.ports[2], client.addr.IP, client.client.natPorts[1]) // C2 -> C1 504 | } 505 | } 506 | } 507 | } 508 | } 509 | 510 | func closeConnection(key *net.TCPConn, info *MessageInfo) { 511 | c, ok := connections[key] 512 | if !ok { 513 | return 514 | } 515 | if info != nil { 516 | c.w <- info 517 | } 518 | close(c.w) 519 | delete(connections, key) 520 | if c.client != nil { 521 | return 522 | } 523 | for key, client := range connections { 524 | if client.client == nil { 525 | continue 526 | } 527 | for _, relay := range client.client.relays { 528 | if relay == c { 529 | closeConnection(key, &MessageInfo{ 530 | MessageType: 0, 531 | Message: "Internal error: Relay disconnected during the test.", 532 | }) 533 | break 534 | } 535 | } 536 | } 537 | } 538 | 539 | func unique(a []int) bool { 540 | for i, v1 := range a { 541 | for v2 := range a[i+1:] { 542 | if v1 == v2 { 543 | return false 544 | } 545 | } 546 | } 547 | return true 548 | } 549 | --------------------------------------------------------------------------------