├── 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 [](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 | 
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 |
--------------------------------------------------------------------------------