├── fuzz └── stun-setters │ └── corpus │ ├── da39a3ee5e6b4b0d3255bfef95601890afd80709 │ ├── 0f55c9c46e8d23f9735dca97bab4775c4d681ba7-1 │ ├── 14a0653bc0fca348b6438edb03120b36ecde7102-1 │ ├── ff905c3903e80d4c8be5bd0e55d318be846195fe-2 │ ├── e80d8798bcff31c7102c317b26cd658528a8ab8b-2 │ ├── 708d940d37311c12d29f5b768f016fe62838224b-3 │ └── 846bf59a50e413462ceb654e778a498623b31e50-4 ├── e2e └── webrtc-chrome │ ├── .gitignore │ ├── .dockerignore │ ├── signaling │ ├── go.mod │ ├── go.sum │ └── main.go │ ├── capture.sh │ ├── tcpdump.Dockerfile │ ├── static │ ├── index.html │ └── script.js │ ├── Makefile │ ├── signaling.Dockerfile │ ├── go.mod │ ├── Dockerfile │ ├── docker-compose.yml │ ├── test.sh │ ├── go.sum │ └── main.go ├── AUTHORS ├── .gitignore ├── gather_test.go ├── .codecov.yml ├── go.mod ├── sdp_fuzz.go ├── internal ├── net.go └── net_test.go ├── go.test.sh ├── .github └── workflows │ ├── lint.yml │ └── go.yml ├── .editorconfig ├── usecandidate_test.go ├── usecandidate.go ├── CONTRIBUTORS ├── priority.go ├── priority_test.go ├── ice.go ├── sdp ├── testdata │ └── candidates_ex1.sdp ├── sdp_test.go └── sdp.go ├── gather ├── gather_test.go └── gather.go ├── cmd └── ice-gather │ └── main.go ├── Makefile ├── agent_transaction_test.go ├── LICENSE ├── golden_test.go ├── gather.go ├── README.md ├── .golangci.yml ├── icecontrol.go ├── candidate └── candidate.go ├── agent_option.go ├── agent_transaction.go ├── pair_test.go ├── icecontrol_test.go ├── checklist_test.go ├── host.go ├── checklist.go ├── candidate.go ├── candidate_test.go ├── host_test.go ├── pair.go ├── agent_gather.go ├── go.sum ├── agent_binding.go └── agent.go /fuzz/stun-setters/corpus/da39a3ee5e6b4b0d3255bfef95601890afd80709: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fuzz/stun-setters/corpus/0f55c9c46e8d23f9735dca97bab4775c4d681ba7-1: -------------------------------------------------------------------------------- 1 |   ̈a -------------------------------------------------------------------------------- /fuzz/stun-setters/corpus/14a0653bc0fca348b6438edb03120b36ecde7102-1: -------------------------------------------------------------------------------- 1 | a22be8b05 -------------------------------------------------------------------------------- /fuzz/stun-setters/corpus/ff905c3903e80d4c8be5bd0e55d318be846195fe-2: -------------------------------------------------------------------------------- 1 | a=2be8b05 -------------------------------------------------------------------------------- /e2e/webrtc-chrome/.gitignore: -------------------------------------------------------------------------------- 1 | e2e 2 | signaling/signaling 3 | log-*.txt 4 | dump.pcap 5 | webrtc-chrome 6 | -------------------------------------------------------------------------------- /fuzz/stun-setters/corpus/e80d8798bcff31c7102c317b26cd658528a8ab8b-2: -------------------------------------------------------------------------------- 1 | シェル フランヘクタールペソ ペニヒ ヘルツ ペンス ページ ベータ ポイA -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Aleksandr Razumov 2 | Aliaksandr Valialkin 3 | Cydev 4 | The IETF Trust 5 | The Go Authors 6 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | *.Dockerfile 3 | .dockerignore 4 | *.pcap 5 | 6 | webrtc-chrome 7 | signaling/signaling -------------------------------------------------------------------------------- /e2e/webrtc-chrome/signaling/go.mod: -------------------------------------------------------------------------------- 1 | module gortc.io/ice/webrtc-chrome/signaling 2 | 3 | go 1.12 4 | 5 | require github.com/gorilla/websocket v1.4.1 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | mem.out 3 | cpu.out 4 | benchmark.*.write 5 | *.test 6 | bench.go-* 7 | PACKAGES 8 | 9 | coverage.txt 10 | profile.out 11 | stun-candidate-fuzz.zip -------------------------------------------------------------------------------- /fuzz/stun-setters/corpus/708d940d37311c12d29f5b768f016fe62838224b-3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gortc/ice/HEAD/fuzz/stun-setters/corpus/708d940d37311c12d29f5b768f016fe62838224b-3 -------------------------------------------------------------------------------- /fuzz/stun-setters/corpus/846bf59a50e413462ceb654e778a498623b31e50-4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gortc/ice/HEAD/fuzz/stun-setters/corpus/846bf59a50e413462ceb654e778a498623b31e50-4 -------------------------------------------------------------------------------- /gather_test.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import "testing" 4 | 5 | func TestGather(t *testing.T) { 6 | _, err := Gather() 7 | if err != nil { 8 | t.Fatal(err) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | project: 5 | default: 6 | # basic 7 | target: 65 8 | threshold: null 9 | base: auto 10 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/capture.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "net: $INTERFACE $SUBNET" 3 | tcpdump -U -v -i $INTERFACE \ 4 | src net $SUBNET and dst net $SUBNET and udp \ 5 | -w /root/dump/dump.pcap 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gortc.io/ice 2 | 3 | go 1.12 4 | 5 | require ( 6 | go.uber.org/zap v1.16.0 7 | gortc.io/sdp v0.18.2 8 | gortc.io/stun v1.23.0 9 | gortc.io/turn v0.11.2 10 | gortc.io/turnc v0.2.0 11 | ) 12 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/tcpdump.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | RUN apt-get update && apt-get install -y tcpdump 3 | RUN apt-get install net-tools -y 4 | 5 | ADD capture.sh /root/capture.sh 6 | ENTRYPOINT ["/bin/bash", "/root/capture.sh"] 7 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ice: e2e 4 | 5 | 6 |

E2E Test

7 |

Intended to run in CI

8 | 9 | 10 | -------------------------------------------------------------------------------- /sdp_fuzz.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | 3 | package ice 4 | 5 | import "gortc.io/ice/sdp" 6 | 7 | var c = new(sdp.Candidate) 8 | 9 | func FuzzCandidate(data []byte) int { 10 | c.Reset() 11 | if err := sdp.ParseAttribute(data, c); err != nil { 12 | return 0 13 | } 14 | return 1 15 | } 16 | -------------------------------------------------------------------------------- /internal/net.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "net" 4 | 5 | // MustParseNet ensures that n is correctly parsed *net.IPNet. 6 | func MustParseNet(n string) *net.IPNet { 7 | _, parsedNet, err := net.ParseCIDR(n) 8 | if err != nil { 9 | panic(err) 10 | } 11 | return parsedNet 12 | } 13 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/Makefile: -------------------------------------------------------------------------------- 1 | start-controlled: 2 | ./webrtc-chrome -addr 127.0.0.1:5568 -signaling 127.0.0.1:2255 -timeout 300s 3 | start-controlling: 4 | ./webrtc-chrome -browser -headless=false -addr 127.0.0.1:5569 -signaling 127.0.0.1:2255 -controlling -timeout 300s -controlled 127.0.0.1:5568 5 | start-signalling: 6 | go run signaling/main.go 7 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/signaling.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG CI_GO_VERSION 2 | FROM golang:${CI_GO_VERSION} 3 | 4 | # Downloading ice deps. 5 | ADD signaling/go.mod /signaling/go.mod 6 | ADD signaling/go.sum /signaling/go.sum 7 | WORKDIR /signaling 8 | RUN go mod download 9 | 10 | ADD signaling/main.go . 11 | RUN go install . 12 | 13 | CMD ["signaling"] 14 | -------------------------------------------------------------------------------- /go.test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | touch coverage.txt 5 | 6 | # quick-test without -race 7 | go test ./... 8 | 9 | for d in $(go list ./... | grep -v vendor); do 10 | go test -race -coverprofile=profile.out -covermode=atomic "$d" 11 | if [ -f profile.out ]; then 12 | cat profile.out >> coverage.txt 13 | rm profile.out 14 | fi 15 | done 16 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/signaling/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= 2 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 3 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 4 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci 2 | on: [push, pull_request] 3 | jobs: 4 | golangci: 5 | name: lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Check out code into the Go module directory 9 | uses: actions/checkout@v1 10 | - name: golangci-lint 11 | uses: golangci/golangci-lint-action@v0.2.0 12 | with: 13 | version: v1.26 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | end_of_line = lf 10 | 11 | [*.go] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [{*.yml,*.yaml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.sh] 20 | indent_style = space 21 | indent_size = 4 22 | 23 | [Makefile] 24 | indent_style = tab 25 | -------------------------------------------------------------------------------- /internal/net_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "testing" 4 | 5 | func TestMustParseNet(t *testing.T) { 6 | t.Run("Negative", func(t *testing.T) { 7 | defer func() { 8 | if recover() == nil { 9 | t.Error("should panic") 10 | } 11 | }() 12 | MustParseNet("___") 13 | }) 14 | t.Run("Positive", func(t *testing.T) { 15 | net := MustParseNet("0.0.0.0/0") 16 | if net.String() != "0.0.0.0/0" { 17 | t.Errorf("bad net %s", net) 18 | } 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.13.x, 1.14.x] 8 | platform: [ubuntu-latest, macos-latest] 9 | runs-on: ${{ matrix.platform }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v1 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Test 18 | run: go test -race ./... 19 | -------------------------------------------------------------------------------- /usecandidate_test.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "testing" 5 | 6 | "gortc.io/stun" 7 | ) 8 | 9 | func TestUseCandidateAttr_AddTo(t *testing.T) { 10 | m := new(stun.Message) 11 | if UseCandidate.IsSet(m) { 12 | t.Error("should not be set") 13 | } 14 | if err := m.Build(stun.BindingRequest, UseCandidate); err != nil { 15 | t.Error(err) 16 | } 17 | m1 := new(stun.Message) 18 | if _, err := m1.Write(m.Raw); err != nil { 19 | t.Error(err) 20 | } 21 | if !UseCandidate.IsSet(m1) { 22 | t.Error("should be set") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/go.mod: -------------------------------------------------------------------------------- 1 | module gortc.io/ice/e2e/webrtc-chrome 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/chromedp/cdproto v0.0.0-20180731224315-b8925c84f3c4 // indirect 7 | github.com/chromedp/chromedp v0.1.2 8 | github.com/gorilla/websocket v1.4.1 9 | github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5 // indirect 10 | github.com/pkg/errors v0.8.1 11 | go.uber.org/zap v1.15.0 12 | golang.org/x/net v0.0.0-20190926025831-c00fd9afed17 13 | gortc.io/ice v0.0.1 14 | gortc.io/sdp v0.18.0 15 | ) 16 | 17 | replace gortc.io/ice v0.0.1 => ../../ 18 | -------------------------------------------------------------------------------- /usecandidate.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import "gortc.io/stun" 4 | 5 | // UseCandidateAttr represents USE-CANDIDATE attribute. 6 | type UseCandidateAttr struct{} 7 | 8 | // AddTo adds USE-CANDIDATE attribute to message. 9 | func (UseCandidateAttr) AddTo(m *stun.Message) error { 10 | m.Add(stun.AttrUseCandidate, nil) 11 | return nil 12 | } 13 | 14 | // IsSet returns true if USE-CANDIDATE attribute is set. 15 | func (UseCandidateAttr) IsSet(m *stun.Message) bool { 16 | _, err := m.Get(stun.AttrUseCandidate) 17 | return err == nil 18 | } 19 | 20 | // UseCandidate is shorthand for UseCandidateAttr. 21 | var UseCandidate UseCandidateAttr 22 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the official list of people who can contribute 2 | # (and typically have contributed) code to this repository. 3 | # 4 | # Names should be added to this file like so: 5 | # Individual's name 6 | # Individual's name 7 | # 8 | # An entry with multiple email addresses specifies that the 9 | # first address should be used in the submit logs and 10 | # that the other addresses should be recognized as the 11 | # same person when interacting with Gerrit. 12 | 13 | # Please keep the list sorted. 14 | 15 | Aleksandr Razumov 16 | Michiel De Backker 17 | Sean DuBois 18 | -------------------------------------------------------------------------------- /priority.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import "gortc.io/stun" 4 | 5 | // PriorityAttr represents PRIORITY attribute. 6 | type PriorityAttr uint32 7 | 8 | const prioritySize = 4 // 32 bit 9 | 10 | // AddTo adds PRIORITY attribute to message. 11 | func (p PriorityAttr) AddTo(m *stun.Message) error { 12 | v := make([]byte, prioritySize) 13 | bin.PutUint32(v, uint32(p)) 14 | m.Add(stun.AttrPriority, v) 15 | return nil 16 | } 17 | 18 | // GetFrom decodes PRIORITY attribute from message. 19 | func (p *PriorityAttr) GetFrom(m *stun.Message) error { 20 | v, err := m.Get(stun.AttrPriority) 21 | if err != nil { 22 | return err 23 | } 24 | if err = stun.CheckSize(stun.AttrPriority, len(v), prioritySize); err != nil { 25 | return err 26 | } 27 | *p = PriorityAttr(bin.Uint32(v)) 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG CI_GO_VERSION 2 | FROM golang:${CI_GO_VERSION} 3 | 4 | # Downloading ice deps. 5 | ADD go.mod /ice/go.mod 6 | ADD go.sum /ice/go.sum 7 | WORKDIR /ice 8 | RUN go mod download 9 | 10 | # Downloading e2e-specific deps. 11 | ADD e2e/webrtc-chrome/go.mod /ice/e2e/webrtc-chrome/go.mod 12 | ADD e2e/webrtc-chrome/go.sum /ice/e2e/webrtc-chrome/go.sum 13 | WORKDIR /ice/e2e/webrtc-chrome/ 14 | RUN go mod download 15 | 16 | ADD . /ice 17 | 18 | RUN go build -o e2e . 19 | 20 | ADD . /ice/ 21 | WORKDIR /ice/e2e/webrtc-chrome 22 | ADD e2e/webrtc-chrome/main.go . 23 | RUN go build -o e2e . 24 | 25 | FROM yukinying/chrome-headless-browser 26 | COPY --from=0 /ice/e2e/webrtc-chrome . 27 | COPY e2e/webrtc-chrome/static static 28 | ENTRYPOINT ["./e2e", "-b=/usr/bin/google-chrome-unstable", "-timeout=3s"] 29 | -------------------------------------------------------------------------------- /priority_test.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "testing" 5 | 6 | "gortc.io/stun" 7 | ) 8 | 9 | func TestPriority_GetFrom(t *testing.T) { 10 | m := new(stun.Message) 11 | var p PriorityAttr 12 | if err := p.GetFrom(m); err != stun.ErrAttributeNotFound { 13 | t.Error("unexpected error") 14 | } 15 | if err := m.Build(stun.BindingRequest, &p); err != nil { 16 | t.Error(err) 17 | } 18 | m1 := new(stun.Message) 19 | if _, err := m1.Write(m.Raw); err != nil { 20 | t.Error(err) 21 | } 22 | var p1 PriorityAttr 23 | if err := p1.GetFrom(m1); err != nil { 24 | t.Error(err) 25 | } 26 | if p1 != p { 27 | t.Error("not equal") 28 | } 29 | t.Run("IncorrectSize", func(t *testing.T) { 30 | m3 := new(stun.Message) 31 | m3.Add(stun.AttrPriority, make([]byte, 100)) 32 | var p2 PriorityAttr 33 | if err := p2.GetFrom(m3); !stun.IsAttrSizeInvalid(err) { 34 | t.Error("should error") 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /ice.go: -------------------------------------------------------------------------------- 1 | // Package ice implements RFC 8445 2 | // Interactive Connectivity Establishment (ICE): 3 | // A Protocol for Network Address Translator (NAT) Traversal 4 | package ice 5 | 6 | import "encoding/binary" 7 | 8 | // bin is shorthand for BigEndian. 9 | var bin = binary.BigEndian 10 | 11 | // State represents the ICE agent state. 12 | // 13 | // As per RFC 8445 Section 6.1.3, the ICE agent has a state determined by the 14 | // state of the checklists. The state is Completed if all checklists are 15 | // Completed, Failed if all checklists are Failed, or Running otherwise. 16 | type State byte 17 | 18 | const ( 19 | // Running if all checklists are nor completed not failed. 20 | Running State = iota 21 | // Completed if all checklists are completed. 22 | Completed 23 | // Failed if all checklists are failed. 24 | Failed 25 | ) 26 | 27 | var stateToStr = map[State]string{ 28 | Running: "Running", 29 | Completed: "Completed", 30 | Failed: "Failed", 31 | } 32 | 33 | func (s State) String() string { return stateToStr[s] } 34 | -------------------------------------------------------------------------------- /sdp/testdata/candidates_ex1.sdp: -------------------------------------------------------------------------------- 1 | a=candidate:3862931549 1 udp 2113937151 192.168.220.128 56032 typ host generation 0 network-cost 50 alpha beta ?? 2 | a=candidate:842163049 1 udp 1677729535 213.141.156.236 55726 typ srflx raddr 3 | a=candidate:2983135859 1 udp 2113937151 10.1.22.220 56024 typ host generation 0 ufrag eM2ytqY8D5Q07RAn 4 | a=candidate:4294175796 1 udp 2113939711 2001:67c:56c:100::3 36737 typ host generation 0 ufrag eM2ytqY8D5Q07RAn 5 | a=candidate:2983135859 2 udp 2113937150 10.1.22.220 51941 typ host generation 0 ufrag eM2ytqY8D5Q07RAn 6 | a=candidate:4294175796 2 udp 2113939710 2001:67c:56c:100::3 42279 typ host generation 0 ufrag eM2ytqY8D5Q07RAn 7 | a=candidate:842163049 2 udp 1677729534 91.225.236.99 51941 typ srflx raddr 10.1.22.220 rport 51941 generation 0 ufrag eM2ytqY8D5Q07RAn 8 | a=candidate:842163049 1 udp 1677729535 91.225.236.99 56024 typ srflx raddr 10.1.22.220 rport 56024 generation 0 ufrag eM2ytqY8D5Q07RAn 9 | a=candidate:842163049 1 udp 1677729535 b2.cydev.ru 56024 typ srflx raddr 10.1.22.220 rport 56024 generation 0 ufrag eM2ytqY8D5Q07RAn -------------------------------------------------------------------------------- /gather/gather_test.go: -------------------------------------------------------------------------------- 1 | package gather 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func TestAddr_String(t *testing.T) { 9 | for _, tt := range []struct { 10 | in Addr 11 | out string 12 | }{ 13 | {in: Addr{}, out: " [0]"}, 14 | {in: Addr{Zone: "z"}, out: " (zone z) [0]"}, 15 | } { 16 | t.Run(tt.out, func(t *testing.T) { 17 | if tt.in.String() != tt.out { 18 | t.Errorf("%q", tt.in) 19 | } 20 | }) 21 | } 22 | } 23 | 24 | func TestAddr_ZeroPortAddr(t *testing.T) { 25 | for _, tt := range []struct { 26 | in Addr 27 | out string 28 | }{ 29 | {in: Addr{}, out: ":0"}, 30 | {in: Addr{Zone: "z"}, out: "%z:0"}, 31 | {in: Addr{Zone: "z", IP: net.IPv4(127, 0, 0, 1)}, out: "127.0.0.1%z:0"}, 32 | } { 33 | t.Run(tt.out, func(t *testing.T) { 34 | if tt.in.ZeroPortAddr() != tt.out { 35 | t.Errorf("%q", tt.in.ZeroPortAddr()) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestDefaultGatherer_Gather(t *testing.T) { 42 | _, err := DefaultGatherer.Gather() 43 | if err != nil { 44 | t.Error(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmd/ice-gather/main.go: -------------------------------------------------------------------------------- 1 | // Command gathers local addresses and prints them. 2 | package main 3 | 4 | import ( 5 | "encoding/hex" 6 | "fmt" 7 | "log" 8 | "net" 9 | 10 | "gortc.io/ice" 11 | "gortc.io/ice/candidate" 12 | ) 13 | 14 | func main() { 15 | addrs, err := ice.Gather() 16 | if err != nil { 17 | log.Fatal("failed to gather: ", err) 18 | } 19 | for _, a := range addrs { 20 | if !ice.IsHostIPValid(a.IP, false) { 21 | continue 22 | } 23 | fmt.Printf("%s\n", a) 24 | laddr, err := net.ResolveUDPAddr("udp", 25 | a.ZeroPortAddr(), 26 | ) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | c, err := net.ListenUDP("udp", laddr) 31 | if err != nil { 32 | fmt.Println(" ", "failed:", err) 33 | continue 34 | } 35 | listenAddr := c.LocalAddr().(*net.UDPAddr) 36 | addr := ice.Addr{ 37 | IP: listenAddr.IP, 38 | Port: listenAddr.Port, 39 | Proto: candidate.UDP, 40 | } 41 | ct := &ice.Candidate{ 42 | Addr: addr, 43 | Base: addr, 44 | Type: candidate.Host, 45 | } 46 | ct.Foundation = ice.Foundation(ct, ice.Addr{}) 47 | fmt.Println(" ", "bind ok", c.LocalAddr(), "0x"+hex.EncodeToString(ct.Foundation)) 48 | _ = c.Close() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(shell git describe --tags | sed -e 's/^v//g' | awk -F "-" '{print $$1}') 2 | ITERATION := $(shell git describe --tags --long | awk -F "-" '{print $$2}') 3 | GO_VERSION=$(shell gobuild -v) 4 | GO := $(or $(GOROOT),/usr/lib/go)/bin/go 5 | PROCS := $(shell nproc) 6 | cores: 7 | @echo "cores: $(PROCS)" 8 | test: 9 | ./go.test.sh 10 | bench: 11 | go test -bench . 12 | bench-record: 13 | $(GO) test -bench . > "benchmarks/stun-go-$(GO_VERSION).txt" 14 | fuzz-prepare-candidate: 15 | go-fuzz-build -func FuzzCandidate -o stun-candidate-fuzz.zip github.com/gortc/ice 16 | fuzz-candidate: 17 | go-fuzz -bin=./stun-candidate-fuzz.zip -workdir=fuzz/stun-setters 18 | lint: 19 | @golangci-lint run ./... 20 | @echo "ok" 21 | escape: 22 | @echo "Not escapes, except autogenerated:" 23 | @go build -gcflags '-m -l' 2>&1 \ 24 | | grep -v "" \ 25 | | grep escapes 26 | format: 27 | goimports -w . 28 | install: 29 | go get -u github.com/golangci/golangci-lint/cmd/golangci-lint 30 | go get -u github.com/dvyukov/go-fuzz/go-fuzz-build 31 | go get github.com/dvyukov/go-fuzz/go-fuzz 32 | test-integration: 33 | @cd integration-test && bash ./test.sh 34 | prepush: test lint test-integration 35 | check-api: 36 | api -c api/ice1.txt github.com/gortc/ice 37 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/signaling/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "sync" 8 | 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | var ( 13 | httpAddr = flag.String("addr", "0.0.0.0:2255", "http endpoint to listen") 14 | ) 15 | 16 | var ws = websocket.Upgrader{ 17 | ReadBufferSize: 1024, 18 | WriteBufferSize: 1024, 19 | CheckOrigin: func(r *http.Request) bool { 20 | return true 21 | }, 22 | } 23 | 24 | var ( 25 | connMux = new(sync.Mutex) 26 | connections []*websocket.Conn 27 | ) 28 | 29 | func main() { 30 | flag.Parse() 31 | http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 32 | log.Println("WS:", r.RemoteAddr) 33 | h := http.Header{} 34 | h.Add("Access-Control-Allow-Origin", "http://127.0.0.1:8080") 35 | conn, err := ws.Upgrade(w, r, h) 36 | if err != nil { 37 | log.Fatalln(err) 38 | } 39 | connMux.Lock() 40 | connections = append(connections, conn) 41 | connMux.Unlock() 42 | go func() { 43 | for { 44 | t, msg, err := conn.ReadMessage() 45 | if err != nil { 46 | break 47 | } 48 | connMux.Lock() 49 | for _, lCon := range connections { 50 | if lCon == conn { 51 | continue 52 | } 53 | lCon.WriteMessage(t, msg) 54 | } 55 | connMux.Unlock() 56 | log.Println("broadcast:", string(msg), "from", conn.RemoteAddr()) 57 | } 58 | }() 59 | }) 60 | log.Fatal(http.ListenAndServe(*httpAddr, nil)) 61 | } 62 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | signaling: 5 | container_name: ci_signaling_1 6 | build: 7 | context: . 8 | dockerfile: signaling.Dockerfile 9 | args: 10 | CI_GO_VERSION: ${CI_GO_VERSION} 11 | environment: 12 | - CI_GO_VERSION 13 | turn-controlling: 14 | container_name: ci_ice-controlling_1 15 | entrypoint: 16 | - ./e2e 17 | - "-b=/usr/bin/google-chrome-unstable" 18 | - "-timeout=10s" 19 | - "-controlling" 20 | - "-browser" 21 | depends_on: 22 | - signaling 23 | links: 24 | - turn-controlled 25 | - signaling 26 | build: 27 | context: ./../.. 28 | dockerfile: e2e/webrtc-chrome/Dockerfile 29 | args: 30 | CI_GO_VERSION: ${CI_GO_VERSION} 31 | cap_add: 32 | - SYS_ADMIN 33 | shm_size: 1024m 34 | environment: 35 | - CI_GO_VERSION 36 | turn-controlled: 37 | container_name: ci_ice-controlled_1 38 | entrypoint: 39 | - ./e2e 40 | - "-b=/usr/bin/google-chrome-unstable" 41 | - "-timeout=10s" 42 | depends_on: 43 | - signaling 44 | links: 45 | - signaling 46 | build: 47 | context: ./../.. 48 | dockerfile: e2e/webrtc-chrome/Dockerfile 49 | args: 50 | CI_GO_VERSION: ${CI_GO_VERSION} 51 | cap_add: 52 | - SYS_ADMIN 53 | shm_size: 1024m 54 | environment: 55 | - CI_GO_VERSION 56 | networks: 57 | default: 58 | external: 59 | name: ice_e2e_webrtc 60 | -------------------------------------------------------------------------------- /agent_transaction_test.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "gortc.io/stun" 8 | ) 9 | 10 | func TestAgent_Timeout(t *testing.T) { 11 | t.Run("NoRetry", func(t *testing.T) { 12 | a, err := NewAgent() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | a.checklist = 0 17 | a.set = ChecklistSet{ 18 | { 19 | Pairs: Pairs{ 20 | {}, 21 | }, 22 | }, 23 | } 24 | now := time.Now() 25 | at := &agentTransaction{ 26 | id: stun.NewTransactionID(), 27 | rto: time.Millisecond * 100, 28 | start: now, 29 | attempt: 1, 30 | maxAttempts: 1, 31 | pair: getPairKey(&a.set[0].Pairs[0]), 32 | } 33 | a.t[at.id] = at 34 | a.collect(at.deadline) 35 | _, ok := a.t[at.id] 36 | if ok { 37 | t.Error("transaction should be removed") 38 | } 39 | }) 40 | t.Run("Retry", func(t *testing.T) { 41 | a, err := NewAgent() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | a.checklist = 0 46 | a.set = ChecklistSet{ 47 | { 48 | Pairs: Pairs{ 49 | {}, 50 | }, 51 | }, 52 | } 53 | now := time.Now() 54 | at := &agentTransaction{ 55 | id: stun.NewTransactionID(), 56 | rto: time.Millisecond * 100, 57 | start: now, 58 | attempt: 2, 59 | maxAttempts: 3, 60 | pair: getPairKey(&a.set[0].Pairs[0]), 61 | } 62 | a.t[at.id] = at 63 | a.collect(at.deadline) 64 | _, ok := a.t[at.id] 65 | if !ok { 66 | t.Error("transaction should be kept") 67 | } 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017-2018 gortc. All Rights Reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /golden_test.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "testing" 10 | ) 11 | 12 | var writeGolden = flag.Bool("golden", false, "write golden files") 13 | 14 | func packageDir(t *testing.T) string { 15 | _, filename, _, ok := runtime.Caller(0) 16 | if !ok { 17 | t.Fatal("unable to get package directory") 18 | } 19 | return filepath.Dir(filename) 20 | } 21 | 22 | func goldenName(t *testing.T, name string) string { 23 | return filepath.Join(packageDir(t), "_testdata", name) 24 | } 25 | 26 | func readGolden(t *testing.T, name string) (*os.File, func()) { 27 | f, err := os.Open(goldenName(t, name)) 28 | if err != nil { 29 | t.Fatal("failed to read", err) 30 | } 31 | return f, func() { 32 | if err = f.Close(); err != nil { 33 | t.Error(err) 34 | } 35 | } 36 | } 37 | 38 | func createGolden(t *testing.T, name string) (*os.File, func()) { 39 | t.Helper() 40 | f, err := os.Create(filepath.Join("_testdata", name)) 41 | if err != nil { 42 | t.Fatal("failed to create", err) 43 | } 44 | t.Logf("golden file created: %s", f.Name()) 45 | return f, func() { 46 | if err = f.Close(); err != nil { 47 | t.Error(err) 48 | } 49 | } 50 | } 51 | 52 | func saveGoldenJSON(t *testing.T, v interface{}, name string) { 53 | t.Helper() 54 | fUpd, closeFUpd := createGolden(t, name) 55 | defer closeFUpd() 56 | e := json.NewEncoder(fUpd) 57 | e.SetIndent("", " ") 58 | if err := e.Encode(v); err != nil { 59 | t.Fatal(err) 60 | } 61 | } 62 | 63 | func loadGoldenJSON(t *testing.T, v interface{}, name string) { 64 | f, closeF := readGolden(t, name) 65 | defer closeF() 66 | d := json.NewDecoder(f) 67 | if err := d.Decode(v); err != nil { 68 | t.Fatal(err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /gather.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "net" 5 | 6 | ct "gortc.io/ice/candidate" 7 | "gortc.io/ice/gather" 8 | ) 9 | 10 | // Gather via DefaultGatherer. 11 | func Gather() ([]gather.Addr, error) { 12 | return gather.DefaultGatherer.Gather() 13 | } 14 | 15 | type systemCandidateGatherer struct { 16 | addr gather.Gatherer 17 | } 18 | 19 | func (g systemCandidateGatherer) gatherUDP(opt gathererOptions) ([]*localUDPCandidate, error) { 20 | addrs, err := g.addr.Gather() 21 | if err != nil { 22 | // Failed to gather host addresses. 23 | return nil, err 24 | } 25 | hostAddr, err := HostAddresses(addrs) 26 | if err != nil { 27 | return nil, err 28 | } 29 | var candidates []*localUDPCandidate 30 | for component := 1; component <= opt.Components; component++ { 31 | for _, addr := range hostAddr { 32 | if opt.IPv4Only && addr.IP.To4() == nil { 33 | continue 34 | } 35 | zeroPort := net.UDPAddr{ 36 | IP: addr.IP, 37 | Port: 0, 38 | } 39 | l, err := net.ListenPacket("udp", zeroPort.String()) 40 | if err != nil { 41 | return nil, err 42 | } 43 | a := l.LocalAddr().(*net.UDPAddr) 44 | c := Candidate{ 45 | Base: Addr{ 46 | IP: addr.IP, 47 | Port: a.Port, 48 | Proto: ct.UDP, 49 | }, 50 | Type: ct.Host, 51 | Addr: Addr{ 52 | IP: addr.IP, 53 | Port: a.Port, 54 | Proto: ct.UDP, 55 | }, 56 | ComponentID: component, 57 | LocalPreference: addr.LocalPreference, 58 | } 59 | c.Foundation = Foundation(&c, Addr{}) 60 | c.Priority = Priority(TypePreference(c.Type), addr.LocalPreference, c.ComponentID) 61 | candidates = append(candidates, &localUDPCandidate{ 62 | candidate: c, 63 | conn: l, 64 | }) 65 | } 66 | } 67 | return candidates, nil 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Master status](https://tc.gortc.io/app/rest/builds/buildType:(id:ice_MasterStatus)/statusIcon.svg)](https://tc.gortc.io/project.html?projectId=ice&tab=projectOverview&guest=1) 2 | [![GoDoc](https://godoc.org/github.com/gortc/ice?status.svg)](http://godoc.org/github.com/gortc/ice) 3 | [![codecov](https://codecov.io/gh/gortc/ice/branch/master/graph/badge.svg)](https://codecov.io/gh/gortc/ice) 4 | # ICE 5 | Package ice implements Interactive Connectivity Establishment (ICE) [[RFC8445](https://tools.ietf.org/html/rfc8445)]: 6 | A Protocol for Network Address Translator (NAT) Traversal. 7 | Complies to [gortc principles](https://gortc.io/#principles) as core package. 8 | 9 | Currently in active development, so no guarantees for API backward 10 | compatibility. 11 | 12 | ## Supported RFCs 13 | - [ ] [RFC 8445](https://tools.ietf.org/html/rfc8445) — Interactive Connectivity Establishment 14 | - [ ] Basic 15 | - [ ] Full 16 | - [ ] [Trickle](https://tools.ietf.org/html/draft-ietf-ice-trickle) 17 | - [x] [RFC 8421](https://tools.ietf.org/html/rfc8421) — Guidelines for Multihomed/Dual-Stack ICE 18 | - [ ] [ice-sip-sdp-21](https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-21) — SDP Offer/Answer for ICE ([sdp](https://godoc.org/github.com/gortc/ice/sdp) subpackage) 19 | - [x] candidate 20 | - [ ] remote candidate 21 | - [ ] ice-lite 22 | - [ ] ice-mismatch 23 | - [ ] ice-pwd 24 | - [ ] ice-ufrag 25 | - [ ] ice-options 26 | - [ ] ice-pacing 27 | - [ ] [RFC 6544](https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis) — TCP Candidates with ICE 28 | - [ ] [rtcweb-19](https://tools.ietf.org/html/draft-ietf-rtcweb-overview-19) — WebRTC 29 | - [ ] [rtcweb-transports-17](https://tools.ietf.org/html/draft-ietf-rtcweb-transports-17) — Transports 30 | 31 | ## Build status 32 | 33 | [![Build Status](https://travis-ci.com/gortc/ice.svg)](https://travis-ci.com/gortc/ice) 34 | [![Master status](https://tc.gortc.io/app/rest/builds/buildType:(id:ice_MasterStatus)/statusIcon.svg)](https://tc.gortc.io/project.html?projectId=ice&tab=projectOverview&guest=1) 35 | [![Go Report](https://goreportcard.com/badge/github.com/gortc/ice)](http://goreportcard.com/report/gortc/ice) 36 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CURRENT_GO_VERSION=$(echo -n "$(go version)" | grep -o 'go1\.[0-9|\.]*' || true) 4 | CURRENT_GO_VERSION=${CURRENT_GO_VERSION#go} 5 | GO_VERSION=${GO_VERSION:-$CURRENT_GO_VERSION} 6 | 7 | # set golang version from env 8 | export CI_GO_VERSION="${GO_VERSION:-latest}" 9 | 10 | # define some colors to use for output 11 | RED='\033[0;31m' 12 | GREEN='\033[0;32m' 13 | NC='\033[0m' 14 | 15 | printf "${GREEN}Go version \"${CI_GO_VERSION}\"${NC}\n" 16 | 17 | # kill and remove any running containers 18 | cleanup () { 19 | docker stop ci_ice_tcpdump 20 | docker rm -f ci_ice_tcpdump 21 | docker-compose --no-ansi -p ci kill 22 | docker-compose --no-ansi -p ci rm -f 23 | docker network rm ice_e2e_webrtc 24 | } 25 | 26 | # catch unexpected failures, do cleanup and output an error message 27 | trap 'cleanup ; printf "${RED}Tests Failed For Unexpected Reasons${NC}\n"'\ 28 | HUP INT QUIT PIPE TERM 29 | 30 | # PREPARING NETWORK CAPTURE 31 | docker network create ice_e2e_webrtc --internal 32 | docker build -t gortc/tcpdump -f tcpdump.Dockerfile . 33 | 34 | NETWORK_ID=`docker network inspect ice_e2e_webrtc -f "{{.Id}}"` 35 | NETWORK_SUBNET=`docker network inspect ice_e2e_webrtc -f "{{(index .IPAM.Config 0).Subnet}}"` 36 | CAPTURE_INTERFACE="br-${NETWORK_ID:0:12}" 37 | 38 | echo "will capture traffic on $CAPTURE_INTERFACE$" 39 | 40 | docker run -e INTERFACE=${CAPTURE_INTERFACE} -e SUBNET=${NETWORK_SUBNET} -d \ 41 | -v $(pwd):/root/dump \ 42 | --name ci_ice_tcpdump --net=host gortc/tcpdump 43 | 44 | 45 | # build and run the composed services 46 | docker-compose --no-ansi -p ci build --parallel && docker-compose --no-ansi -p ci up -d 47 | if [ $? -ne 0 ] ; then 48 | printf "${RED}Docker Compose Failed${NC}\n" 49 | exit -1 50 | fi 51 | 52 | # wait for the test service to complete and grab the exit code 53 | TEST_EXIT_CODE=`docker wait ci_ice-controlling_1` 54 | 55 | # output the logs for the test (for clarity) 56 | docker logs ci_ice-controlling_1 &> log-controlling.txt 57 | docker logs ci_ice-controlled_1 &> log-controlled.txt 58 | docker logs ci_ice_tcpdump &> log-tcpdump.txt 59 | 60 | cat log-controlling.txt 61 | 62 | # inspect the output of the test and display respective message 63 | if [ -z ${TEST_EXIT_CODE+x} ] || [ "$TEST_EXIT_CODE" -ne 0 ] ; then 64 | printf "${RED}Tests Failed${NC} - Exit Code: $TEST_EXIT_CODE\n" 65 | printf "${GREEN}Logs from turn server:${NC}\n" 66 | cat log-controlled.txt 67 | else 68 | printf "${GREEN}Tests Passed${NC}\n" 69 | fi 70 | 71 | # call the cleanup function 72 | cleanup 73 | 74 | # exit the script with the same code as the test service code 75 | exit ${TEST_EXIT_CODE} 76 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | govet: 3 | check-shadowing: true 4 | golint: 5 | min-confidence: 0 6 | gocyclo: 7 | min-complexity: 15 8 | funlen: 9 | lines: 100 10 | statements: 50 11 | maligned: 12 | suggest-new: true 13 | dupl: 14 | threshold: 100 15 | goconst: 16 | min-len: 2 17 | min-occurrences: 3 18 | misspell: 19 | locale: US 20 | lll: 21 | line-length: 140 22 | goimports: 23 | local-prefixes: github.com/gortc 24 | gocritic: 25 | enabled-tags: 26 | - performance 27 | - style 28 | - experimental 29 | disabled-checks: 30 | - sloppyReassign 31 | - hugeParam 32 | 33 | issues: 34 | exclude-use-default: false 35 | exclude: 36 | - "isOptional is a pure function" 37 | - "should have a package comment, unless it's in another file for this package" 38 | exclude-rules: 39 | - text: "string `(Unknown||UDP)`" 40 | linters: [goconst] 41 | 42 | - text: \(\*candidateParser\) 43 | linters: [gocyclo] 44 | 45 | - linters: [dupl] 46 | source: "gather\\S+CandidatesFor" 47 | 48 | # Exclude some linters from running on tests files. 49 | - path: _test\.go 50 | linters: 51 | - gocyclo 52 | - errcheck 53 | - dupl 54 | - gosec 55 | - goconst 56 | - unparam 57 | - funlen 58 | - gocyclo 59 | - gocognit 60 | 61 | # Ease some gocritic warnings on test files. 62 | - path: _test\.go 63 | text: "(unnamedResult|exitAfterDefer|unlambda)" 64 | linters: [gocritic] 65 | 66 | - path: ^cmd/ 67 | linters: [gocyclo] 68 | - path: ^cmd/ 69 | text: "(unnamedResult|exitAfterDefer)" 70 | linters: [gocritic] 71 | 72 | # RFC references 73 | - linters: [godot] 74 | source: "RFC \\d+" 75 | - linters: [godot] 76 | source: " #nosec" 77 | # Code comments 78 | - linters: [godot] 79 | source: "\\/\\/\\s+}" 80 | 81 | linters: 82 | enable-all: true 83 | disable: 84 | - gochecknoglobals 85 | - scopelint 86 | - gochecknoinits 87 | - prealloc 88 | - gomnd 89 | - wsl 90 | - godox 91 | - testpackage 92 | - goerr113 93 | 94 | run: 95 | skip-dirs: 96 | - e2e 97 | - fuzz 98 | - testdata 99 | - _testdata 100 | - api 101 | 102 | # golangci.com configuration 103 | # https://github.com/golangci/golangci/wiki/Configuration 104 | service: 105 | golangci-lint-version: 1.15.x # use fixed version to not introduce new linters unexpectedly 106 | prepare: 107 | - echo "here I can run custom commands, but no preparation needed" 108 | -------------------------------------------------------------------------------- /icecontrol.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import "gortc.io/stun" 4 | 5 | // tiebreaker is common helper for ICE-{CONTROLLED,CONTROLLING} 6 | // and represents the so-called tiebreaker number. 7 | type tiebreaker uint64 8 | 9 | const tiebreakerSize = 8 // 64 bit 10 | 11 | // AddToAs adds tiebreaker value to m as t attribute. 12 | func (a tiebreaker) AddToAs(m *stun.Message, t stun.AttrType) error { 13 | v := make([]byte, tiebreakerSize) 14 | bin.PutUint64(v, uint64(a)) 15 | m.Add(t, v) 16 | return nil 17 | } 18 | 19 | // GetFromAs decodes tiebreaker value in message getting it as for t type. 20 | func (a *tiebreaker) GetFromAs(m *stun.Message, t stun.AttrType) error { 21 | v, err := m.Get(t) 22 | if err != nil { 23 | return err 24 | } 25 | if err = stun.CheckSize(t, len(v), tiebreakerSize); err != nil { 26 | return err 27 | } 28 | *a = tiebreaker(bin.Uint64(v)) 29 | return nil 30 | } 31 | 32 | // AttrControlled represents ICE-CONTROLLED attribute. 33 | type AttrControlled uint64 34 | 35 | // AddTo adds ICE-CONTROLLED to message. 36 | func (c AttrControlled) AddTo(m *stun.Message) error { 37 | return tiebreaker(c).AddToAs(m, stun.AttrICEControlled) 38 | } 39 | 40 | // GetFrom decodes ICE-CONTROLLED from message. 41 | func (c *AttrControlled) GetFrom(m *stun.Message) error { 42 | return (*tiebreaker)(c).GetFromAs(m, stun.AttrICEControlled) 43 | } 44 | 45 | // AttrControlling represents ICE-CONTROLLING attribute. 46 | type AttrControlling uint64 47 | 48 | // AddTo adds ICE-CONTROLLING to message. 49 | func (c AttrControlling) AddTo(m *stun.Message) error { 50 | return tiebreaker(c).AddToAs(m, stun.AttrICEControlling) 51 | } 52 | 53 | // GetFrom decodes ICE-CONTROLLING from message. 54 | func (c *AttrControlling) GetFrom(m *stun.Message) error { 55 | return (*tiebreaker)(c).GetFromAs(m, stun.AttrICEControlling) 56 | } 57 | 58 | // AttrControl is helper that wraps ICE-{CONTROLLED,CONTROLLING}. 59 | type AttrControl struct { 60 | Role Role 61 | Tiebreaker uint64 62 | } 63 | 64 | // AddTo adds ICE-CONTROLLED or ICE-CONTROLLING attribute depending on Role. 65 | func (c AttrControl) AddTo(m *stun.Message) error { 66 | if c.Role == Controlling { 67 | return tiebreaker(c.Tiebreaker).AddToAs(m, stun.AttrICEControlling) 68 | } 69 | return tiebreaker(c.Tiebreaker).AddToAs(m, stun.AttrICEControlled) 70 | } 71 | 72 | // GetFrom decodes Role and Tiebreaker value from message. 73 | func (c *AttrControl) GetFrom(m *stun.Message) error { 74 | if m.Contains(stun.AttrICEControlling) { 75 | c.Role = Controlling 76 | return (*tiebreaker)(&c.Tiebreaker).GetFromAs(m, stun.AttrICEControlling) 77 | } 78 | if m.Contains(stun.AttrICEControlled) { 79 | c.Role = Controlled 80 | return (*tiebreaker)(&c.Tiebreaker).GetFromAs(m, stun.AttrICEControlled) 81 | } 82 | return stun.ErrAttributeNotFound 83 | } 84 | -------------------------------------------------------------------------------- /candidate/candidate.go: -------------------------------------------------------------------------------- 1 | // Package candidate contains common types for ice candidate. 2 | package candidate 3 | 4 | import "fmt" 5 | 6 | // Type encodes the type of candidate. This specification 7 | // defines the values "host", "srflx", "prflx", and "relay" for host, 8 | // server reflexive, peer reflexive, and relayed candidates, 9 | // respectively. The set of candidate types is extensible for the 10 | // future. 11 | type Type byte 12 | 13 | // UnmarshalText implements TextUnmarshaler. 14 | func (t *Type) UnmarshalText(text []byte) error { 15 | for k, v := range candidateTypeToStr { 16 | if string(text) == v { 17 | *t = k 18 | return nil 19 | } 20 | } 21 | return fmt.Errorf("unknown candidate type value: %q", text) 22 | } 23 | 24 | // MarshalText implements TextMarshaler. 25 | func (t Type) MarshalText() (text []byte, err error) { 26 | return []byte(candidateTypeToStr[t]), nil 27 | } 28 | 29 | // Set of possible candidate types. 30 | const ( 31 | // Host is a candidate obtained by binding to a specific port 32 | // from an IP address on the host. This includes IP addresses on 33 | // physical interfaces and logical ones, such as ones obtained 34 | // through VPNs. 35 | Host Type = iota 36 | // ServerReflexive is a candidate whose IP address and port 37 | // are a binding allocated by a NAT for an ICE agent after it sends a 38 | // packet through the NAT to a server, such as a STUN server. 39 | ServerReflexive 40 | // PeerReflexive is a candidate whose IP address and port are 41 | // a binding allocated by a NAT for an ICE agent after it sends a 42 | // packet through the NAT to its peer. 43 | PeerReflexive 44 | // Relayed is a candidate obtained from a relay server, such as 45 | // a TURN server. 46 | Relayed 47 | ) 48 | 49 | var candidateTypeToStr = map[Type]string{ 50 | Host: "Host", 51 | ServerReflexive: "Server-reflexive", 52 | PeerReflexive: "Peer-reflexive", 53 | Relayed: "Relayed", 54 | } 55 | 56 | func strOrUnknown(str string) string { 57 | if str == "" { 58 | return "Unknown" 59 | } 60 | return str 61 | } 62 | 63 | func (t Type) String() string { 64 | return strOrUnknown(candidateTypeToStr[t]) 65 | } 66 | 67 | // Protocol is protocol for address. 68 | type Protocol byte 69 | 70 | // UnmarshalText implements TextUnmarshaler. 71 | func (t *Protocol) UnmarshalText(s []byte) error { 72 | switch string(s) { 73 | case "udp", "UDP": 74 | *t = UDP 75 | default: 76 | *t = ProtocolUnknown 77 | } 78 | return nil 79 | } 80 | 81 | // MarshalText implements TextMarshaler. 82 | func (t Protocol) MarshalText() ([]byte, error) { 83 | return []byte(t.String()), nil 84 | } 85 | 86 | // Supported protocols. 87 | const ( 88 | UDP Protocol = iota 89 | ProtocolUnknown 90 | ) 91 | 92 | func (t Protocol) String() string { 93 | switch t { 94 | case UDP: 95 | return "UDP" 96 | default: 97 | return "Unknown" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /agent_option.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "time" 7 | 8 | "go.uber.org/zap" 9 | 10 | "gortc.io/stun" 11 | "gortc.io/turn" 12 | ) 13 | 14 | // AgentOption represents configuration option for Agent. 15 | type AgentOption func(a *Agent) error 16 | 17 | // WithRole sets agent mode to Controlling or Controlled. 18 | func WithRole(r Role) AgentOption { 19 | return func(a *Agent) error { 20 | a.role = r 21 | return nil 22 | } 23 | } 24 | 25 | // WithLogger sets *zap.Logger for Agent. 26 | func WithLogger(l *zap.Logger) AgentOption { 27 | return func(a *Agent) error { 28 | a.log = l 29 | return nil 30 | } 31 | } 32 | 33 | // WithServer configures ICE server or servers for Agent. 34 | func WithServer(servers ...Server) AgentOption { 35 | return func(a *Agent) error { 36 | for _, s := range servers { 37 | for _, uri := range s.URI { 38 | if strings.HasPrefix(uri, stun.Scheme) { 39 | u, err := stun.ParseURI(uri) 40 | if err != nil { 41 | return err 42 | } 43 | a.stun = append(a.stun, stunServerOptions{ 44 | username: s.Username, 45 | password: s.Credential, 46 | uri: u, 47 | }) 48 | } else { 49 | u, err := turn.ParseURI(uri) 50 | if err != nil { 51 | return err 52 | } 53 | a.turn = append(a.turn, turnServerOptions{ 54 | username: s.Username, 55 | password: s.Credential, 56 | uri: u, 57 | }) 58 | } 59 | } 60 | } 61 | return nil 62 | } 63 | } 64 | 65 | // WithSTUN configures Agent to use STUN server. 66 | // 67 | // Use WithServer to add STUN with credentials or multiple servers at once. 68 | func WithSTUN(uri string) AgentOption { 69 | return func(a *Agent) error { 70 | u, err := stun.ParseURI(uri) 71 | if err != nil { 72 | return err 73 | } 74 | a.stun = append(a.stun, stunServerOptions{ 75 | uri: u, 76 | }) 77 | return nil 78 | } 79 | } 80 | 81 | // WithTURN configures Agent to use TURN server. 82 | // 83 | // Use WithServer to add multiple servers at once. 84 | func WithTURN(uri, username, credential string) AgentOption { 85 | return func(a *Agent) error { 86 | u, err := turn.ParseURI(uri) 87 | if err != nil { 88 | return err 89 | } 90 | a.turn = append(a.turn, turnServerOptions{ 91 | password: credential, 92 | username: username, 93 | uri: u, 94 | }) 95 | return nil 96 | } 97 | } 98 | 99 | // WithIPv4Only enables IPv4-only mode, where IPv6 candidates are not used. 100 | var WithIPv4Only AgentOption = func(a *Agent) error { 101 | a.ipv4Only = true 102 | return nil 103 | } 104 | 105 | // WithTa sets Ta timer value which is technically time between candidates. 106 | func WithTa(ta time.Duration) AgentOption { 107 | return func(a *Agent) error { 108 | if ta < 0 { 109 | return errors.New("ta should be positive") 110 | } 111 | a.ta = ta 112 | return nil 113 | } 114 | } 115 | 116 | // WithMaxAttempts sets maximum attempts. 117 | func WithMaxAttempts(n int) AgentOption { 118 | return func(a *Agent) error { 119 | a.maxAttempts = n 120 | return nil 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /agent_transaction.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "time" 7 | 8 | "go.uber.org/zap" 9 | 10 | "gortc.io/stun" 11 | ) 12 | 13 | type transactionID [stun.TransactionIDSize]byte 14 | 15 | func (t transactionID) AddTo(m *stun.Message) error { 16 | m.TransactionID = t 17 | return nil 18 | } 19 | 20 | // agentTransaction represents transaction in progress. 21 | // 22 | // Concurrent access is invalid. 23 | type agentTransaction struct { 24 | checklist int 25 | pair pairKey 26 | priority int 27 | nominate bool 28 | id transactionID 29 | start time.Time 30 | rto time.Duration 31 | deadline time.Time 32 | raw []byte 33 | attempt int 34 | maxAttempts int 35 | } 36 | 37 | func (t *agentTransaction) setDeadline(now time.Time) { 38 | t.deadline = now.Add(time.Duration(t.attempt) * t.rto) 39 | } 40 | 41 | // handleTimeout handles maximum attempts reached state for transaction, 42 | // updating the pair states to failed. 43 | func (a *Agent) handleTimeout(t *agentTransaction) error { 44 | a.mux.Lock() 45 | p, ok := a.getPair(t.checklist, t.pair) 46 | if !ok { 47 | a.mux.Unlock() 48 | return errors.New("no pair found") 49 | } 50 | cl := a.set[t.checklist] 51 | for i := range cl.Triggered { 52 | if samePair(&cl.Triggered[i], p) { 53 | cl.Triggered[i].State = PairFailed 54 | } 55 | } 56 | for i := range cl.Pairs { 57 | if samePair(&cl.Pairs[i], p) { 58 | cl.Pairs[i].State = PairFailed 59 | } 60 | } 61 | a.mux.Unlock() 62 | return nil 63 | } 64 | 65 | // retry re-sends same binding request to associated candidate. 66 | func (a *Agent) retry(t *agentTransaction) { 67 | a.mux.Lock() 68 | p, ok := a.getPair(t.checklist, t.pair) 69 | a.mux.Unlock() 70 | if !ok { 71 | a.log.Warn("failed to pick pair for retry") 72 | return 73 | } 74 | c, ok := a.localCandidateByAddr(p.Local.Addr) 75 | if !ok { 76 | a.log.Warn("failed to pick local candidate for retry") 77 | return 78 | } 79 | udpAddr := &net.UDPAddr{ 80 | IP: p.Remote.Addr.IP, 81 | Port: p.Remote.Addr.Port, 82 | } 83 | _, err := c.conn.WriteTo(t.raw, udpAddr) 84 | if err != nil { 85 | a.log.Error("failed to write", zap.Error(err)) 86 | } 87 | } 88 | 89 | const defaultTransactionCap = 30 90 | 91 | // collect handles transaction timeouts, performing retry or updating the 92 | // pair state if max attempts reached. 93 | func (a *Agent) collect(now time.Time) { 94 | toHandle := make([]*agentTransaction, 0, defaultTransactionCap) 95 | toDelete := make([]transactionID, 0, defaultTransactionCap) 96 | 97 | a.tMux.Lock() 98 | for id, t := range a.t { 99 | if !t.deadline.After(now) { 100 | toDelete = append(toDelete, id) 101 | toHandle = append(toHandle, t) 102 | } 103 | } 104 | for _, id := range toDelete { 105 | delete(a.t, id) 106 | } 107 | a.tMux.Unlock() 108 | 109 | if len(toHandle) == 0 { 110 | return 111 | } 112 | 113 | toRetry := make([]*agentTransaction, 0, defaultTransactionCap) 114 | for _, t := range toHandle { 115 | if t.attempt < t.maxAttempts { 116 | t.attempt++ 117 | t.setDeadline(now) 118 | toRetry = append(toRetry, t) 119 | continue 120 | } 121 | if err := a.handleTimeout(t); err != nil { 122 | a.log.Error("failed to handle timeout", zap.Error(err)) 123 | } 124 | } 125 | 126 | a.tMux.Lock() 127 | for _, t := range toRetry { 128 | a.t[t.id] = t 129 | } 130 | a.tMux.Unlock() 131 | 132 | for _, t := range toRetry { 133 | a.retry(t) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /pair_test.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sort" 7 | "testing" 8 | ) 9 | 10 | func TestPairPriority(t *testing.T) { 11 | for _, tc := range []struct { 12 | G, D int 13 | Value int64 14 | }{ 15 | {0, 0, 0}, 16 | {1, 1, 4294967298}, 17 | {1, 2, 4294967300}, 18 | {2, 1, 4294967301}, 19 | } { 20 | t.Run(fmt.Sprintf("%d_%d", tc.G, tc.D), func(t *testing.T) { 21 | if v := PairPriority(tc.G, tc.D); v != tc.Value { 22 | t.Errorf("%d (got) != %d (expected)", v, tc.Value) 23 | } 24 | }) 25 | } 26 | } 27 | 28 | func TestPair_Foundation(t *testing.T) { 29 | p := Pair{ 30 | Local: Candidate{ 31 | Foundation: make([]byte, foundationLength), 32 | }, 33 | Remote: Candidate{ 34 | Foundation: make([]byte, foundationLength), 35 | }, 36 | } 37 | p.SetFoundation() 38 | f := p.Foundation 39 | if len(f) != foundationLength*2 { 40 | t.Error("bad length") 41 | } 42 | } 43 | 44 | func TestPairs(t *testing.T) { 45 | pairs := Pairs{ 46 | {Priority: 4}, 47 | {Priority: 3}, 48 | {Priority: 100}, 49 | {Priority: 0, ComponentID: 2}, 50 | {Priority: 0, ComponentID: 1}, 51 | {Priority: 4}, 52 | {Priority: 5}, 53 | {Priority: 9}, 54 | {Priority: 8}, 55 | } 56 | sort.Sort(pairs) 57 | expectedOrder := []struct { 58 | priority int64 59 | component int 60 | }{ 61 | {100, 0}, 62 | {9, 0}, 63 | {8, 0}, 64 | {5, 0}, 65 | {4, 0}, 66 | {4, 0}, 67 | {3, 0}, 68 | {0, 1}, 69 | {0, 2}, 70 | } 71 | for i, p := range pairs { 72 | if p.Priority != expectedOrder[i].priority { 73 | t.Errorf("p[%d]: %d (got) != %d (expected)", i, p.Priority, expectedOrder[i]) 74 | } 75 | if p.ComponentID != expectedOrder[i].component { 76 | t.Errorf("p[%d] component: %d (got) != %d (expected)", i, p.Priority, expectedOrder[i]) 77 | } 78 | } 79 | } 80 | 81 | func TestNewPairs(t *testing.T) { 82 | for _, tc := range []struct { 83 | Name string 84 | Local Candidates 85 | Remote Candidates 86 | Result Pairs 87 | }{ 88 | { 89 | Name: "Blank", 90 | }, 91 | { 92 | Name: "No pairs", 93 | Local: Candidates{ 94 | { 95 | Addr: Addr{ 96 | IP: net.ParseIP("1.1.1.1"), 97 | }, 98 | }, 99 | }, 100 | Remote: Candidates{ 101 | { 102 | Addr: Addr{ 103 | IP: net.ParseIP("2001:11:12:13:14:15:16:17"), 104 | }, 105 | }, 106 | }, 107 | }, 108 | { 109 | Name: "Simple", 110 | Local: Candidates{ 111 | { 112 | Addr: Addr{ 113 | IP: net.ParseIP("1.1.1.1"), 114 | }, 115 | }, 116 | }, 117 | Remote: Candidates{ 118 | { 119 | Addr: Addr{ 120 | IP: net.ParseIP("1.1.1.2"), 121 | }, 122 | }, 123 | }, 124 | Result: Pairs{ 125 | { 126 | Local: Candidate{ 127 | Addr: Addr{ 128 | IP: net.ParseIP("1.1.1.1"), 129 | }, 130 | }, 131 | Remote: Candidate{ 132 | Addr: Addr{ 133 | IP: net.ParseIP("1.1.1.2"), 134 | }, 135 | }, 136 | }, 137 | }, 138 | }, 139 | } { 140 | t.Run(tc.Name, func(t *testing.T) { 141 | got := NewPairs(tc.Local, tc.Remote) 142 | if len(got) != len(tc.Result) { 143 | t.Fatalf("bad length: %d (got) != %d (expected)", len(got), len(tc.Result)) 144 | } 145 | for i := range tc.Result { 146 | expectedAddr := tc.Result[i].Remote.Addr 147 | gotAddr := got[i].Remote.Addr 148 | if !gotAddr.Equal(expectedAddr) { 149 | t.Errorf("[%d]: remote addr mismatch: %s (got) != %s (expected)", i, gotAddr, expectedAddr) 150 | } 151 | expectedAddr = tc.Result[i].Local.Addr 152 | gotAddr = got[i].Local.Addr 153 | if !gotAddr.Equal(expectedAddr) { 154 | t.Errorf("[%d]: local addr mismatch: %s (got) != %s (expected)", i, gotAddr, expectedAddr) 155 | } 156 | } 157 | }) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /icecontrol_test.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "testing" 5 | 6 | "gortc.io/stun" 7 | ) 8 | 9 | func TestControlled_GetFrom(t *testing.T) { 10 | m := new(stun.Message) 11 | var c AttrControlled 12 | if err := c.GetFrom(m); err != stun.ErrAttributeNotFound { 13 | t.Error("unexpected error") 14 | } 15 | if err := m.Build(stun.BindingRequest, &c); err != nil { 16 | t.Error(err) 17 | } 18 | m1 := new(stun.Message) 19 | if _, err := m1.Write(m.Raw); err != nil { 20 | t.Error(err) 21 | } 22 | var c1 AttrControlled 23 | if err := c1.GetFrom(m1); err != nil { 24 | t.Error(err) 25 | } 26 | if c1 != c { 27 | t.Error("not equal") 28 | } 29 | t.Run("IncorrectSize", func(t *testing.T) { 30 | m3 := new(stun.Message) 31 | m3.Add(stun.AttrICEControlled, make([]byte, 100)) 32 | var c2 AttrControlled 33 | if err := c2.GetFrom(m3); !stun.IsAttrSizeInvalid(err) { 34 | t.Error("should error") 35 | } 36 | }) 37 | } 38 | 39 | func TestControlling_GetFrom(t *testing.T) { 40 | m := new(stun.Message) 41 | var c AttrControlling 42 | if err := c.GetFrom(m); err != stun.ErrAttributeNotFound { 43 | t.Error("unexpected error") 44 | } 45 | if err := m.Build(stun.BindingRequest, &c); err != nil { 46 | t.Error(err) 47 | } 48 | m1 := new(stun.Message) 49 | if _, err := m1.Write(m.Raw); err != nil { 50 | t.Error(err) 51 | } 52 | var c1 AttrControlling 53 | if err := c1.GetFrom(m1); err != nil { 54 | t.Error(err) 55 | } 56 | if c1 != c { 57 | t.Error("not equal") 58 | } 59 | t.Run("IncorrectSize", func(t *testing.T) { 60 | m3 := new(stun.Message) 61 | m3.Add(stun.AttrICEControlling, make([]byte, 100)) 62 | var c2 AttrControlling 63 | if err := c2.GetFrom(m3); !stun.IsAttrSizeInvalid(err) { 64 | t.Error("should error") 65 | } 66 | }) 67 | } 68 | 69 | func TestControl_GetFrom(t *testing.T) { 70 | t.Run("Blank", func(t *testing.T) { 71 | m := new(stun.Message) 72 | var c AttrControl 73 | if err := c.GetFrom(m); err != stun.ErrAttributeNotFound { 74 | t.Error("unexpected error") 75 | } 76 | }) 77 | t.Run("Controlling", func(t *testing.T) { 78 | m := new(stun.Message) 79 | var c AttrControl 80 | if err := c.GetFrom(m); err != stun.ErrAttributeNotFound { 81 | t.Error("unexpected error") 82 | } 83 | c.Role = Controlling 84 | c.Tiebreaker = 4321 85 | if err := m.Build(stun.BindingRequest, &c); err != nil { 86 | t.Error(err) 87 | } 88 | m1 := new(stun.Message) 89 | if _, err := m1.Write(m.Raw); err != nil { 90 | t.Error(err) 91 | } 92 | var c1 AttrControl 93 | if err := c1.GetFrom(m1); err != nil { 94 | t.Error(err) 95 | } 96 | if c1 != c { 97 | t.Error("not equal") 98 | } 99 | t.Run("IncorrectSize", func(t *testing.T) { 100 | m3 := new(stun.Message) 101 | m3.Add(stun.AttrICEControlling, make([]byte, 100)) 102 | var c2 AttrControl 103 | if err := c2.GetFrom(m3); !stun.IsAttrSizeInvalid(err) { 104 | t.Error("should error") 105 | } 106 | }) 107 | }) 108 | t.Run("Controlled", func(t *testing.T) { 109 | m := new(stun.Message) 110 | var c AttrControl 111 | if err := c.GetFrom(m); err != stun.ErrAttributeNotFound { 112 | t.Error("unexpected error") 113 | } 114 | c.Role = Controlled 115 | c.Tiebreaker = 1234 116 | if err := m.Build(stun.BindingRequest, &c); err != nil { 117 | t.Error(err) 118 | } 119 | m1 := new(stun.Message) 120 | if _, err := m1.Write(m.Raw); err != nil { 121 | t.Error(err) 122 | } 123 | var c1 AttrControl 124 | if err := c1.GetFrom(m1); err != nil { 125 | t.Error(err) 126 | } 127 | if c1 != c { 128 | t.Error("not equal") 129 | } 130 | t.Run("IncorrectSize", func(t *testing.T) { 131 | m3 := new(stun.Message) 132 | m3.Add(stun.AttrICEControlling, make([]byte, 100)) 133 | var c2 AttrControl 134 | if err := c2.GetFrom(m3); !stun.IsAttrSizeInvalid(err) { 135 | t.Error("should error") 136 | } 137 | }) 138 | }) 139 | } 140 | -------------------------------------------------------------------------------- /checklist_test.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | 8 | "gortc.io/ice/candidate" 9 | ) 10 | 11 | func TestChecklistJSON(t *testing.T) { 12 | c := Checklist{ 13 | State: ChecklistCompleted, 14 | Pairs: Pairs{ 15 | { 16 | Local: Candidate{Priority: 102, Type: candidate.PeerReflexive}, 17 | Remote: Candidate{Priority: 91}, 18 | State: PairSucceeded, 19 | }, 20 | { 21 | Local: Candidate{Priority: 100, Type: candidate.Relayed}, 22 | Remote: Candidate{Priority: 50}, 23 | State: PairWaiting, 24 | }, 25 | { 26 | Local: Candidate{Priority: 103}, 27 | Remote: Candidate{Priority: 93}, 28 | State: PairFrozen, 29 | }, 30 | { 31 | Local: Candidate{Priority: 104}, 32 | Remote: Candidate{Priority: 94}, 33 | State: PairFailed, 34 | }, 35 | { 36 | Local: Candidate{Priority: 101}, 37 | Remote: Candidate{Priority: 90}, 38 | State: PairInProgress, 39 | }, 40 | }, 41 | } 42 | buf := new(bytes.Buffer) 43 | e := json.NewEncoder(buf) 44 | if err := e.Encode(c); err != nil { 45 | t.Fatal(err) 46 | } 47 | d := json.NewDecoder(buf) 48 | var cGot Checklist 49 | if err := d.Decode(&cGot); err != nil { 50 | t.Fatal(err) 51 | } 52 | if !cGot.Equal(c) { 53 | t.Error("not equal") 54 | } 55 | } 56 | 57 | func TestChecklist_Order(t *testing.T) { 58 | c := Checklist{ 59 | Pairs: Pairs{ 60 | {Priority: 1}, 61 | {Priority: 10}, 62 | }, 63 | } 64 | c.Sort() 65 | if c.Pairs[0].Priority == 1 { 66 | t.Error("pair with 1 priority should be second") 67 | } 68 | } 69 | 70 | func TestChecklist_ComputePriorities(t *testing.T) { 71 | c := Checklist{ 72 | Pairs: Pairs{ 73 | { 74 | Local: Candidate{Priority: 102}, 75 | Remote: Candidate{Priority: 91}, 76 | }, 77 | { 78 | Local: Candidate{Priority: 100}, 79 | Remote: Candidate{Priority: 50}, 80 | }, 81 | { 82 | Local: Candidate{Priority: 103}, 83 | Remote: Candidate{Priority: 93}, 84 | }, 85 | { 86 | Local: Candidate{Priority: 104}, 87 | Remote: Candidate{Priority: 94}, 88 | }, 89 | { 90 | Local: Candidate{Priority: 101}, 91 | Remote: Candidate{Priority: 90}, 92 | }, 93 | }, 94 | } 95 | expectedControlled := []int64{ 96 | 390842024140, 214748365000, 399431958734, 403726926032, 386547056842, 97 | } 98 | expectedControlling := []int64{ 99 | 390842024141, 214748365001, 399431958735, 403726926033, 386547056843, 100 | } 101 | c.ComputePriorities(Controlled) 102 | for i, p := range c.Pairs { 103 | e := expectedControlled[i] 104 | if e != p.Priority { 105 | t.Errorf("controlled: [%d] %d (got) != %d (expected)", 106 | i, p.Priority, e, 107 | ) 108 | } 109 | } 110 | c.ComputePriorities(Controlling) 111 | for i, p := range c.Pairs { 112 | e := expectedControlling[i] 113 | if e != p.Priority { 114 | t.Errorf("controlling: [%d] %d (got) != %d (expected)", 115 | i, p.Priority, e, 116 | ) 117 | } 118 | } 119 | } 120 | 121 | func TestChecklist_Prune(t *testing.T) { 122 | c := Checklist{ 123 | Pairs: Pairs{ 124 | // TODO: Improve this 125 | { 126 | Local: Candidate{}, 127 | Remote: Candidate{}, 128 | }, 129 | { 130 | Local: Candidate{}, 131 | Remote: Candidate{}, 132 | }, 133 | { 134 | Local: Candidate{}, 135 | Remote: Candidate{}, 136 | }, 137 | { 138 | Local: Candidate{}, 139 | Remote: Candidate{}, 140 | }, 141 | { 142 | Local: Candidate{}, 143 | Remote: Candidate{}, 144 | }, 145 | }, 146 | } 147 | c.Prune() 148 | if len(c.Pairs) != 1 { 149 | t.Error("unexpected result length") 150 | } 151 | } 152 | 153 | func TestChecklist_Limit(t *testing.T) { 154 | c := Checklist{ 155 | Pairs: Pairs{ 156 | { 157 | Priority: 100, 158 | }, 159 | { 160 | Priority: 99, 161 | }, 162 | { 163 | Priority: 98, 164 | }, 165 | { 166 | Priority: 97, 167 | }, 168 | { 169 | Priority: 96, 170 | }, 171 | }, 172 | } 173 | c.Limit(10) 174 | if c.Len() != 5 { 175 | t.Error("unexpected length") 176 | } 177 | c.Limit(3) 178 | if c.Len() != 3 { 179 | t.Error("unexpected length") 180 | } 181 | } 182 | 183 | func TestChecklistState_String(t *testing.T) { 184 | for _, s := range []ChecklistState{ 185 | ChecklistRunning, ChecklistCompleted, ChecklistFailed, 186 | } { 187 | if s.String() == "" { 188 | t.Errorf("checklist iota %d should have String() value", int(s)) 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /gather/gather.go: -------------------------------------------------------------------------------- 1 | package gather 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | "sort" 8 | 9 | "gortc.io/ice/internal" 10 | ) 11 | 12 | const precedencesCount = 11 13 | 14 | var precedences [precedencesCount]precedenceConfig 15 | 16 | type precedenceConfig struct { 17 | ipNet *net.IPNet 18 | value int 19 | } 20 | 21 | // Gatherer is source for addresses. 22 | // 23 | // See RFC 8445 Section 2.1 for details on gathering. 24 | type Gatherer interface { 25 | Gather() ([]Addr, error) 26 | } 27 | 28 | func init() { 29 | // Initializing policy table. 30 | // See RFC 6724 Section 2.1. 31 | /* 32 | ::1/128 50 0 33 | ::/0 40 1 34 | ::ffff:0:0/96 35 4 35 | 2002::/16 30 2 36 | 2001::/32 5 5 37 | fc00::/7 3 13 38 | ::/96 1 3 39 | fec0::/10 1 11 40 | 3ffe::/16 1 12 41 | */ 42 | for i, p := range [precedencesCount]struct { 43 | cidr string 44 | value int 45 | label int 46 | }{ 47 | {"::1/128", 50, 0}, 48 | {"127.0.0.1/8", 45, 0}, 49 | {"::/0", 40, 1}, 50 | {"::ffff:0:0/96", 35, 4}, 51 | {"fe80::/10", 33, 1}, 52 | {"2002::/16", 30, 2}, 53 | {"2001::/32", 5, 5}, 54 | {"fc00::/7", 3, 13}, 55 | {"::/96", 1, 3}, 56 | {"fec0::/10", 1, 11}, 57 | {"3ffe::/16", 1, 12}, 58 | } { 59 | precedences[i] = precedenceConfig{ 60 | ipNet: internal.MustParseNet(p.cidr), 61 | value: p.value, 62 | } 63 | } 64 | } 65 | 66 | // Addr represents gathered address from interface. 67 | type Addr struct { 68 | IP net.IP 69 | Zone string 70 | Precedence int 71 | } 72 | 73 | // Addrs is addr slice helper. 74 | type Addrs []Addr 75 | 76 | func (s Addrs) Less(i, j int) bool { 77 | si, sj := s[i], s[j] 78 | if si.Precedence == sj.Precedence { 79 | // Comparing IP's to make stable sort. 80 | return bytes.Compare(si.IP, sj.IP) < 0 81 | } 82 | return si.Precedence > sj.Precedence 83 | } 84 | 85 | func (s Addrs) Swap(i, j int) { 86 | s[i], s[j] = s[j], s[i] 87 | } 88 | 89 | func (s Addrs) Len() int { 90 | return len(s) 91 | } 92 | 93 | func (a Addr) String() string { 94 | if len(a.Zone) > 0 { 95 | return fmt.Sprintf("%s (zone %s) [%d]", a.IP, a.Zone, a.Precedence) 96 | } 97 | return fmt.Sprintf("%s [%d]", a.IP, a.Precedence) 98 | } 99 | 100 | // ZeroPortAddr return address with "0" port. 101 | func (a Addr) ZeroPortAddr() string { 102 | host := a.IP.String() 103 | if len(a.Zone) > 0 { 104 | host += "%" + a.Zone 105 | } 106 | return net.JoinHostPort(host, "0") 107 | } 108 | 109 | type defaultGatherer struct{} 110 | 111 | // Precedence returns precedence value of ip address defined by RFC 6724. 112 | func Precedence(ip net.IP) int { 113 | for _, p := range precedences { 114 | if p.ipNet.Contains(ip) { 115 | return p.value 116 | } 117 | } 118 | return 0 119 | } 120 | 121 | type netInterface interface { 122 | Addrs() ([]net.Addr, error) 123 | } 124 | 125 | func ifaceToAddr(i netInterface, name string) ([]Addr, error) { 126 | var addrs []Addr 127 | netAddrs, err := i.Addrs() 128 | if err != nil { 129 | return addrs, err 130 | } 131 | for _, a := range netAddrs { 132 | ip, _, err := net.ParseCIDR(a.String()) 133 | if err != nil { 134 | return addrs, err 135 | } 136 | addr := Addr{ 137 | IP: ip, 138 | Precedence: Precedence(ip), 139 | } 140 | if ip.IsLinkLocalUnicast() { 141 | // Zone must be set for link-local addresses. 142 | addr.Zone = name 143 | } 144 | addrs = append(addrs, addr) 145 | } 146 | return addrs, nil 147 | } 148 | 149 | func ifaceValid(iface net.Interface) bool { 150 | f := iface.Flags 151 | if f&net.FlagUp == 0 { 152 | // Interface is down. 153 | return false 154 | } 155 | if f&net.FlagLoopback != 0 { 156 | // Interface is loopback. 157 | return false 158 | } 159 | return true 160 | } 161 | 162 | func (g defaultGatherer) Gather() ([]Addr, error) { 163 | interfaces, err := net.Interfaces() 164 | if err != nil { 165 | return nil, err 166 | } 167 | addrs := make([]Addr, 0, 10) 168 | for _, iface := range interfaces { 169 | if !ifaceValid(iface) { 170 | continue 171 | } 172 | ifaceAddrs, err := ifaceToAddr(&iface, iface.Name) 173 | if err != nil { 174 | return addrs, err 175 | } 176 | addrs = append(addrs, ifaceAddrs...) 177 | } 178 | sort.Sort(Addrs(addrs)) 179 | return addrs, nil 180 | } 181 | 182 | // DefaultGatherer uses net.Interfaces to gather addresses. 183 | var DefaultGatherer Gatherer = defaultGatherer{} 184 | -------------------------------------------------------------------------------- /host.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "net" 5 | 6 | "gortc.io/ice/gather" 7 | "gortc.io/ice/internal" 8 | ) 9 | 10 | // See Deprecating Site Local Addresses [RFC3879]. 11 | var siteLocalIPv6 = internal.MustParseNet("FEC0::/10") 12 | 13 | // IsHostIPValid reports whether ip is valid as host address ip. 14 | func IsHostIPValid(ip net.IP, ipv6Only bool) bool { 15 | var ( 16 | v4 = ip.To4() != nil 17 | v6 = !v4 18 | ) 19 | if v6 && ip.To16() == nil { 20 | return false 21 | } 22 | if v4 && ipv6Only { 23 | // IPv4-mapped IPv6 addresses SHOULD NOT be included in the address 24 | // candidates unless the application using ICE does not support IPv4 25 | // (i.e., it is an IPv6-only application [RFC4038]). 26 | return false 27 | } 28 | if ip.IsLoopback() { 29 | // Addresses from a loopback interface MUST NOT be included in the 30 | // candidate addresses. 31 | return false 32 | } 33 | if siteLocalIPv6.Contains(ip) { 34 | // Deprecated IPv4-compatible IPv6 addresses [RFC4291] and IPv6 site- 35 | // local unicast addresses [RFC3879] MUST NOT be included in the 36 | // address candidates. 37 | return false 38 | } 39 | if ip.IsLinkLocalUnicast() && v6 { 40 | // When host candidates corresponding to an IPv6 address generated 41 | // using a mechanism that prevents location tracking are gathered, then 42 | // host candidates corresponding to IPv6 link-local addresses [RFC4291] 43 | // MUST NOT be gathered. 44 | return false 45 | } 46 | return true 47 | } 48 | 49 | // HostAddr wraps IP of host interface and local preference. 50 | type HostAddr struct { 51 | IP net.IP 52 | LocalPreference int 53 | } 54 | 55 | // v4 and v6 length must be non-zero, and len(v4) + len(v6) must be len(all). 56 | func processDualStack(all, v4, v6 []gather.Addr) []HostAddr { 57 | var ( 58 | v6InARow int 59 | ) 60 | nHi := (len(v6) + len(v4)) / len(v4) 61 | hostAddrs := make([]HostAddr, 0, len(all)) 62 | for i := 0; i < len(all); i++ { 63 | useV6 := true 64 | if v6InARow >= nHi { 65 | v6InARow = 0 66 | useV6 = false 67 | } 68 | pref := len(all) - i 69 | if useV6 && len(v6) > 0 { 70 | v6InARow++ 71 | hostAddrs = append(hostAddrs, HostAddr{ 72 | IP: v6[0].IP, 73 | LocalPreference: pref, 74 | }) 75 | v6 = v6[1:] 76 | } else if len(v4) > 0 { 77 | hostAddrs = append(hostAddrs, HostAddr{ 78 | IP: v4[0].IP, 79 | LocalPreference: pref, 80 | }) 81 | v4 = v4[1:] 82 | } 83 | } 84 | return hostAddrs 85 | } 86 | 87 | func isV6Only(addrs []gather.Addr) bool { 88 | v6Only := true 89 | for _, addr := range addrs { 90 | if addr.IP.To4() != nil { 91 | v6Only = false 92 | break 93 | } 94 | } 95 | return v6Only 96 | } 97 | 98 | func filterValid(gathered []gather.Addr) []gather.Addr { 99 | valid := make([]gather.Addr, 0, len(gathered)) 100 | v6Only := isV6Only(gathered) 101 | for _, addr := range gathered { 102 | if !IsHostIPValid(addr.IP, v6Only) { 103 | continue 104 | } 105 | valid = append(valid, addr) 106 | } 107 | return valid 108 | } 109 | 110 | const ( 111 | // When there is only a single IP address, this value SHOULD be 112 | // set to 65535. 113 | singleIPAddrPreference = 65535 114 | ) 115 | 116 | // HostAddresses returns valid host addresses from gathered addresses with 117 | // calculated local preference. 118 | // 119 | // When gathered addresses are only IPv6, the host is considered ipv6-only. 120 | // When there are both IPv6 and IPv4 addresses, the RFC 8421 is used to 121 | // calculate local preferences. 122 | func HostAddresses(gathered []gather.Addr) ([]HostAddr, error) { 123 | if len(gathered) == 0 { 124 | return []HostAddr{}, nil 125 | } 126 | validOnly := filterValid(gathered) 127 | if len(validOnly) == 0 { 128 | return []HostAddr{}, nil 129 | } 130 | if len(validOnly) == 1 { 131 | // Setting local preference for single IP as defined 132 | // in RFC 8445 Section 5.1.2.1. 133 | return []HostAddr{ 134 | { 135 | IP: validOnly[0].IP, 136 | LocalPreference: singleIPAddrPreference, 137 | }, 138 | }, nil 139 | } 140 | var ( 141 | v6Addrs, v4Addrs []gather.Addr 142 | ) 143 | for _, addr := range validOnly { 144 | if addr.IP.To4() == nil { 145 | v6Addrs = append(v6Addrs, addr) 146 | } else { 147 | v4Addrs = append(v4Addrs, addr) 148 | } 149 | } 150 | if len(v4Addrs) == 0 || len(v6Addrs) == 0 { 151 | // Single-stack and multi-homed. 152 | hostAddrs := make([]HostAddr, 0, len(validOnly)) 153 | for i, a := range validOnly { 154 | hostAddrs = append(hostAddrs, HostAddr{ 155 | IP: a.IP, 156 | LocalPreference: len(validOnly) - i, 157 | }) 158 | } 159 | return hostAddrs, nil 160 | } 161 | // Dual-stack calculation as defined in RFC 8421. 162 | return processDualStack(validOnly, v4Addrs, v6Addrs), nil 163 | } 164 | -------------------------------------------------------------------------------- /checklist.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | // Checklist is set of pairs. 9 | // 10 | // 11 | // From RFC 8455 Section 6.1.2: 12 | // 13 | // There is one checklist for each data stream. To form a checklist, 14 | // initiating and responding ICE agents form candidate pairs, compute 15 | // pair priorities, order pairs by priority, prune pairs, remove lower- 16 | // priority pairs, and set checklist states. If candidates are added to 17 | // a checklist (e.g., due to detection of peer-reflexive candidates), 18 | // the agent will re-perform these steps for the updated checklist. 19 | type Checklist struct { 20 | Pairs Pairs `json:"pairs,omitempty"` 21 | Valid Pairs `json:"valid,omitempty"` 22 | Triggered Pairs `json:"triggered,omitempty"` // FIFO 23 | State ChecklistState `json:"state"` 24 | } 25 | 26 | // Equal returns true if checklist c equals to checklist b. 27 | func (c Checklist) Equal(b Checklist) bool { 28 | if c.State != b.State { 29 | return false 30 | } 31 | if len(c.Pairs) != len(b.Pairs) { 32 | return false 33 | } 34 | for i := range c.Pairs { 35 | if !c.Pairs[i].Equal(&b.Pairs[i]) { 36 | return false 37 | } 38 | } 39 | return true 40 | } 41 | 42 | // ChecklistState represents the Checklist State. 43 | // 44 | // See RFC 8445 Section 6.1.2.1 45 | type ChecklistState byte 46 | 47 | // UnmarshalText implements TextUnmarshaler. 48 | func (s *ChecklistState) UnmarshalText(text []byte) error { 49 | for k, v := range checklistStateToStr { 50 | if string(text) == v { 51 | *s = k 52 | return nil 53 | } 54 | } 55 | return fmt.Errorf("unknown checklist state value: %q", text) 56 | } 57 | 58 | // MarshalText implements TextMarshaler. 59 | func (s ChecklistState) MarshalText() (text []byte, err error) { 60 | return []byte(checklistStateToStr[s]), nil 61 | } 62 | 63 | var checklistStateToStr = map[ChecklistState]string{ 64 | ChecklistRunning: "Running", 65 | ChecklistCompleted: "Completed", 66 | ChecklistFailed: "Failed", 67 | } 68 | 69 | func (s ChecklistState) String() string { return checklistStateToStr[s] } 70 | 71 | const ( 72 | // ChecklistRunning is neither Completed nor Failed yet. Checklists are 73 | // initially set to the Running state. 74 | ChecklistRunning ChecklistState = iota 75 | // ChecklistCompleted contains a nominated pair for each component of the 76 | // data stream. 77 | ChecklistCompleted 78 | // ChecklistFailed does not have a valid pair for each component of the data 79 | // stream, and all of the candidate pairs in the checklist are in either the 80 | // Failed or the Succeeded state. In other words, at least one component of 81 | // the checklist has candidate pairs that are all in the Failed state, which 82 | // means the component has failed, which means the checklist has failed. 83 | ChecklistFailed 84 | ) 85 | 86 | // ComputePriorities computes priorities for all pairs based on agent role. 87 | // 88 | // The role determines whether local candidate is from controlling or from 89 | // controlled agent. 90 | func (c *Checklist) ComputePriorities(role Role) { 91 | for i := range c.Pairs { 92 | var ( 93 | controlling = c.Pairs[i].Local.Priority 94 | controlled = c.Pairs[i].Remote.Priority 95 | ) 96 | if role == Controlled { 97 | controlling, controlled = controlled, controlling 98 | } 99 | c.Pairs[i].Priority = PairPriority(controlling, controlled) 100 | } 101 | } 102 | 103 | // Sort is ordering pairs by priority descending. 104 | // First element will have highest priority. 105 | func (c *Checklist) Sort() { sort.Sort(c.Pairs) } 106 | 107 | // Prune removes redundant candidates. 108 | // 109 | // Two candidate pairs are redundant if their local candidates have the same 110 | // base and their remote candidates are identical. 111 | func (c *Checklist) Prune() { 112 | // Pruning algorithm is not optimal but should work for small numbers, 113 | // where len(c.Pairs) ~ 100. 114 | result := make(Pairs, 0, len(c.Pairs)) 115 | Loop: 116 | for i := range c.Pairs { 117 | base := c.Pairs[i].Local.Base 118 | for j := range result { 119 | // Check if local candidates have the same base. 120 | if !result[j].Local.Base.Equal(base) { 121 | continue 122 | } 123 | // Check if remote candidates are identical. 124 | if !result[j].Remote.Equal(&c.Pairs[i].Remote) { 125 | continue 126 | } 127 | // Pair is redundant, skipping. 128 | continue Loop 129 | } 130 | result = append(result, c.Pairs[i]) 131 | } 132 | c.Pairs = result 133 | } 134 | 135 | // Limit ensures maximum length of pairs, removing the pairs with least priority 136 | // if needed. 137 | func (c *Checklist) Limit(max int) { 138 | if len(c.Pairs) <= max { 139 | return 140 | } 141 | c.Pairs = c.Pairs[:max] 142 | } 143 | 144 | // Len returns pairs count. 145 | func (c *Checklist) Len() int { return len(c.Pairs) } 146 | -------------------------------------------------------------------------------- /candidate.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "fmt" 7 | "net" 8 | 9 | ct "gortc.io/ice/candidate" 10 | ) 11 | 12 | // Addr represents transport address, the combination of an IP address 13 | // and the transport protocol (such as UDP or TCP) port. 14 | type Addr struct { 15 | IP net.IP `json:"ip,omitempty"` 16 | Port int `json:"port,omitempty"` 17 | Proto ct.Protocol `json:"proto,omitempty"` 18 | } 19 | 20 | // Equal returns true of b equals to a. 21 | func (a Addr) Equal(b Addr) bool { 22 | if a.Proto != b.Proto { 23 | return false 24 | } 25 | if a.Port != b.Port { 26 | return false 27 | } 28 | return a.IP.Equal(b.IP) 29 | } 30 | 31 | func (a Addr) String() string { 32 | return fmt.Sprintf("%s:%d/%s", a.IP, a.Port, a.Proto) 33 | } 34 | 35 | // Candidates is list of candidates ordered by priority descending. 36 | type Candidates []Candidate 37 | 38 | func (c Candidates) Len() int { return len(c) } 39 | func (c Candidates) Less(i, j int) bool { return c[i].Priority > c[j].Priority } 40 | func (c Candidates) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 41 | 42 | // The Candidate is a transport address that is a potential point of contact 43 | // for receipt of data. Candidates also have properties — their type 44 | // (server reflexive, relayed, or host), priority, foundation, and base. 45 | type Candidate struct { 46 | Addr Addr `json:"addr"` 47 | Type ct.Type `json:"type"` 48 | Priority int `json:"priority"` 49 | Foundation []byte `json:"foundation"` 50 | Base Addr `json:"base,omitempty"` 51 | Related Addr `json:"related,omitempty"` 52 | ComponentID int `json:"component_id"` 53 | LocalPreference int `json:"local_preference"` 54 | } 55 | 56 | // Equal reports whether c equals to b. 57 | func (c *Candidate) Equal(b *Candidate) bool { 58 | if c.Type != b.Type { 59 | return false 60 | } 61 | if c.Priority != b.Priority { 62 | return false 63 | } 64 | if c.LocalPreference != b.LocalPreference { 65 | return false 66 | } 67 | if c.ComponentID != b.ComponentID { 68 | return false 69 | } 70 | if !c.Addr.Equal(b.Addr) { 71 | return false 72 | } 73 | if !bytes.Equal(c.Foundation, b.Foundation) { 74 | return false 75 | } 76 | if !c.Base.Equal(b.Base) { 77 | return false 78 | } 79 | if !c.Related.Equal(b.Related) { 80 | // Should we skip that check? 81 | return false 82 | } 83 | return true 84 | } 85 | 86 | const foundationLength = 8 87 | 88 | // Foundation computes foundation value for candidate. The serverAddr parameter 89 | // is for STUN or TURN server address, zero value is valid. Will return nil if 90 | // candidate is nil. 91 | // 92 | // Value is an arbitrary string used in the freezing algorithm to 93 | // group similar candidates. It is the same for two candidates that 94 | // have the same type, base IP address, protocol (UDP, TCP, etc.), 95 | // and STUN or TURN server. If any of these are different, then the 96 | // foundation will be different. 97 | func Foundation(c *Candidate, serverAddr Addr) []byte { 98 | if c == nil { 99 | return nil 100 | } 101 | h := sha256.New() 102 | values := [][]byte{{byte(c.Type)}, c.Base.IP, {byte(c.Addr.Proto)}} 103 | if len(serverAddr.IP) > 0 { 104 | values = append(values, serverAddr.IP, []byte{byte(serverAddr.Proto)}) 105 | } 106 | _, _ = h.Write(bytes.Join(values, []byte{':'})) // #nosec 107 | return h.Sum(nil)[:foundationLength] 108 | } 109 | 110 | // The RECOMMENDED values for type preferences are 126 for host 111 | // candidates, 110 for peer-reflexive candidates, 100 for server- 112 | // reflexive candidates, and 0 for relayed candidates. 113 | // 114 | // From RFC 8445 Section 5.1.2.2. 115 | var typePreferences = map[ct.Type]int{ 116 | ct.Host: 126, 117 | ct.PeerReflexive: 110, 118 | ct.ServerReflexive: 100, 119 | ct.Relayed: 0, 120 | } 121 | 122 | // TypePreference returns recommended type preference for candidate type. 123 | func TypePreference(t ct.Type) int { return typePreferences[t] } 124 | 125 | // Priority calculates the priority value by RFC 8445 Section 5.1.2.1 formulae. 126 | // 127 | // The typePref value MUST be an integer from 0 (lowest preference) to 126 128 | // (highest preference) inclusive, MUST be identical for all candidates of 129 | // the same type, and MUST be different for candidates of different types. 130 | // 131 | // The localPref value MUST be an integer from 0 (lowest preference) to 132 | // 65535 (highest preference) inclusive. When there is only a single IP 133 | // address, this value SHOULD be set to 65535. If there are multiple 134 | // candidates for a particular component for a particular data stream 135 | // that have the same type, the local preference MUST be unique for each 136 | // one. If an ICE agent is dual stack, the local preference SHOULD be 137 | // set according to the current best practice described in [RFC8421]. 138 | // 139 | // The component ID MUST be an integer between 1 and 256 inclusive. 140 | func Priority(typePref, localPref, componentID int) int { 141 | // priority = (2^24)*(type preference) + 142 | // (2^8)*(local preference) + 143 | // (2^0)*(256 - component ID) 144 | return (1<<24)*typePref + (1<<8)*localPref + (1<<0)*(256-componentID) 145 | } 146 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/static/script.js: -------------------------------------------------------------------------------- 1 | var peerConnection; 2 | const peerConnectionConfig = {}; 3 | const dataChannelOptions = { 4 | ordered: false, // do not guarantee order 5 | }; 6 | 7 | function pageReady() { 8 | fetch("/config").then(res => res.json()).then(function (myJSON) { 9 | serverConnection = new WebSocket(myJSON.signaling); 10 | serverConnection.onmessage = gotMessageFromServer; 11 | 12 | if (myJSON.controlling) { 13 | start(true); 14 | } else { 15 | serverConnection.onopen = () => { 16 | fetch("/initialized", { 17 | method: "post" 18 | }).catch((reason) => { 19 | console.log("failed to init notify", reason) 20 | }) 21 | } 22 | } 23 | }); 24 | } 25 | 26 | function receiveChannelCallback(event) { 27 | console.log('received data channel'); 28 | const receiveChannel = event.channel; 29 | receiveChannel.onmessage = (event) => { 30 | console.log("dataChannel message:", event.data); 31 | fetch("/success").then(function () { 32 | console.log("success"); 33 | }) 34 | }; 35 | receiveChannel.onopen = () => { 36 | receiveChannel.send("hello [from controlled]"); 37 | }; 38 | receiveChannel.onclose = () => { 39 | console.log("dataChannel closed"); 40 | }; 41 | } 42 | 43 | function start(isCaller) { 44 | peerConnection = new RTCPeerConnection(peerConnectionConfig); 45 | peerConnection.onicecandidate = gotIceCandidate; 46 | peerConnection.ondatachannel = receiveChannelCallback; 47 | peerConnection.onconnectionstatechange = function(event) { 48 | console.log("connection state", peerConnection.connectionState); 49 | switch(peerConnection.connectionState) { 50 | case "connected": 51 | // The connection has become fully connected 52 | break; 53 | case "disconnected": 54 | case "failed": 55 | // One or more transports has terminated unexpectedly or in an error 56 | break; 57 | case "closed": 58 | // The connection has been closed 59 | break; 60 | } 61 | }; 62 | if(isCaller) { 63 | const dataChannel = peerConnection.createDataChannel("matrix", dataChannelOptions); 64 | dataChannel.onerror = (error) => { 65 | console.log("dataChannel error:", error); 66 | }; 67 | dataChannel.onmessage = (event) => { 68 | console.log("dataChannel message:", event.data); 69 | fetch("/success").then(function () { 70 | console.log("success"); 71 | }) 72 | }; 73 | dataChannel.onopen = () => { 74 | console.log("dataChannel opened"); 75 | dataChannel.send("hello [from caller]"); 76 | }; 77 | dataChannel.onclose = () => { 78 | console.log("dataChannel closed"); 79 | }; 80 | peerConnection.createOffer().then(gotDescription).catch(createOfferError); 81 | } 82 | } 83 | 84 | function gotDescription(description) { 85 | console.log('got description'); 86 | peerConnection.setLocalDescription(description).then(function () { 87 | // serverConnection.send(JSON.stringify({'sdp': description})) 88 | // TODO: Use with trickle. 89 | }).catch(function () { 90 | console.log('set description error') 91 | }); 92 | } 93 | 94 | function gotIceCandidate(event) { 95 | if(event.candidate != null) { 96 | serverConnection.send(JSON.stringify({'ice': event.candidate})); 97 | } else { 98 | console.log("local description", peerConnection.localDescription); 99 | serverConnection.send(JSON.stringify({'sdp': peerConnection.localDescription})); 100 | } 101 | } 102 | 103 | function createOfferError(error) { 104 | console.log(error); 105 | } 106 | 107 | function gotMessageFromServer(message) { 108 | if(!peerConnection) start(false); 109 | const signal = JSON.parse(message.data); 110 | if(signal.success) { 111 | // TODO: Remove when data-channels ready. 112 | console.log("forced success"); 113 | fetch("/success").then(function () { 114 | console.log("success"); 115 | }) 116 | } 117 | if(signal.sdp) { 118 | console.log("got remote description", signal.sdp); 119 | const d = new RTCSessionDescription(signal.sdp); 120 | console.log("got remote description", d); 121 | peerConnection.setRemoteDescription(d).then(function() { 122 | console.log("set remote description"); 123 | if(signal.sdp.type === 'offer') { 124 | peerConnection.createAnswer().then(gotDescription).catch(function (err) { 125 | console.log(err); 126 | }); 127 | } else { 128 | serverConnection.send(JSON.stringify({'signal': "gotDescription"})); 129 | } 130 | }).catch(function (reason) { 131 | console.log("failed to set remote description: " + JSON.stringify(reason)) 132 | }); 133 | } else if(signal.ice) { 134 | peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice)).then(function () { 135 | console.log("ice candidate added") 136 | }); 137 | } 138 | } 139 | 140 | pageReady(); -------------------------------------------------------------------------------- /candidate_test.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "sort" 7 | "testing" 8 | 9 | "gortc.io/ice/candidate" 10 | ) 11 | 12 | var localIP = net.IPv4(127, 0, 0, 1) 13 | 14 | func TestFoundation(t *testing.T) { 15 | for _, tc := range []struct { 16 | Name string 17 | A, B *Candidate 18 | AddrA, AddrB Addr 19 | Equal bool 20 | }{ 21 | { 22 | Name: "nil", 23 | Equal: true, 24 | }, 25 | { 26 | Name: "simple", 27 | A: &Candidate{ 28 | Addr: Addr{ 29 | IP: localIP, 30 | Port: 1, 31 | Proto: candidate.UDP, 32 | }, 33 | Base: Addr{ 34 | IP: localIP, 35 | Port: 10, 36 | Proto: candidate.UDP, 37 | }, 38 | }, 39 | B: &Candidate{ 40 | Addr: Addr{ 41 | IP: localIP, 42 | Port: 1, 43 | Proto: candidate.UDP, 44 | }, 45 | Base: Addr{ 46 | IP: localIP, 47 | Port: 10, 48 | Proto: candidate.UDP, 49 | }, 50 | }, 51 | Equal: true, 52 | }, 53 | { 54 | Name: "different turn", 55 | A: &Candidate{ 56 | Addr: Addr{ 57 | IP: localIP, 58 | Port: 1, 59 | Proto: candidate.UDP, 60 | }, 61 | Base: Addr{ 62 | IP: localIP, 63 | Port: 10, 64 | Proto: candidate.UDP, 65 | }, 66 | }, 67 | AddrA: Addr{ 68 | IP: localIP, 69 | }, 70 | B: &Candidate{ 71 | Addr: Addr{ 72 | IP: localIP, 73 | Port: 1, 74 | Proto: candidate.UDP, 75 | }, 76 | Base: Addr{ 77 | IP: localIP, 78 | Port: 10, 79 | Proto: candidate.UDP, 80 | }, 81 | }, 82 | Equal: false, 83 | }, 84 | } { 85 | t.Run(tc.Name, func(t *testing.T) { 86 | a := Foundation(tc.A, tc.AddrA) 87 | b := Foundation(tc.B, tc.AddrB) 88 | t.Logf("a: 0x%x", a) 89 | t.Logf("b: 0x%x", b) 90 | if bytes.Equal(a, b) != tc.Equal { 91 | t.Error("mismatch") 92 | } 93 | }) 94 | } 95 | } 96 | 97 | func TestPriority(t *testing.T) { 98 | for _, tc := range []struct { 99 | Name string 100 | Type int 101 | Local int 102 | ID int 103 | Value int 104 | }{ 105 | { 106 | Name: "host", 107 | Type: 126, 108 | Value: 2113929472, 109 | }, 110 | { 111 | Name: "full", 112 | Type: TypePreference(candidate.PeerReflexive), 113 | Local: 50, 114 | ID: 2, 115 | Value: 1845506814, 116 | }, 117 | { 118 | Name: "relayed", 119 | Type: TypePreference(candidate.Relayed), 120 | Local: 10, 121 | ID: 5, 122 | Value: 2811, 123 | }, 124 | { 125 | Name: "server reflexive", 126 | Type: TypePreference(candidate.ServerReflexive), 127 | Local: 3, 128 | ID: 1, 129 | Value: 1677722623, 130 | }, 131 | } { 132 | t.Run(tc.Name, func(t *testing.T) { 133 | if v := Priority(tc.Type, tc.Local, tc.ID); v != tc.Value { 134 | t.Errorf("p(%d, %d, %d) %d (got) != %d (expected)", 135 | tc.Type, tc.Local, tc.ID, v, tc.Value, 136 | ) 137 | } 138 | }) 139 | } 140 | } 141 | 142 | func TestAddr_String(t *testing.T) { 143 | for _, tc := range []struct { 144 | Addr Addr 145 | String string 146 | }{ 147 | { 148 | String: ":0/UDP", 149 | }, 150 | { 151 | String: "1.1.1.1:10/UDP", 152 | Addr: Addr{ 153 | IP: net.IPv4(1, 1, 1, 1), 154 | Port: 10, 155 | }, 156 | }, 157 | } { 158 | if v := tc.Addr.String(); v != tc.String { 159 | t.Errorf("string(%+v): %s (got) != %s (expected)", 160 | tc.Addr, v, tc.String, 161 | ) 162 | } 163 | } 164 | } 165 | 166 | func TestAddr_Equal(t *testing.T) { 167 | for _, tc := range []struct { 168 | Name string 169 | A, B Addr 170 | Equal bool 171 | }{ 172 | { 173 | Name: "zero", 174 | Equal: true, 175 | }, 176 | { 177 | Name: "proto", 178 | A: Addr{ 179 | Proto: 200, 180 | }, 181 | }, 182 | { 183 | Name: "port", 184 | A: Addr{ 185 | Port: 1, 186 | }, 187 | }, 188 | { 189 | Name: "ip", 190 | A: Addr{ 191 | IP: net.IPv4(1, 1, 1, 1), 192 | }, 193 | }, 194 | } { 195 | if v := tc.A.Equal(tc.B); v != tc.Equal { 196 | t.Errorf("equal(%s, %s): %v (got) != %v (expected)", 197 | tc.A, tc.B, v, tc.Equal, 198 | ) 199 | } 200 | } 201 | } 202 | 203 | func TestCandidate_Equal(t *testing.T) { 204 | for _, tc := range []struct { 205 | Name string 206 | A, B Candidate 207 | Equal bool 208 | }{ 209 | { 210 | Name: "blank", 211 | Equal: true, 212 | }, 213 | { 214 | Name: "type", 215 | B: Candidate{ 216 | Type: candidate.PeerReflexive, 217 | }, 218 | Equal: false, 219 | }, 220 | { 221 | Name: "priority", 222 | B: Candidate{ 223 | Priority: 1000, 224 | }, 225 | Equal: false, 226 | }, 227 | { 228 | Name: "addr", 229 | B: Candidate{ 230 | Addr: Addr{ 231 | IP: net.IPv4(127, 0, 0, 1), 232 | }, 233 | }, 234 | Equal: false, 235 | }, 236 | { 237 | Name: "foundation", 238 | A: Candidate{ 239 | Foundation: []byte{2}, 240 | }, 241 | B: Candidate{ 242 | Foundation: []byte{1}, 243 | }, 244 | Equal: false, 245 | }, 246 | { 247 | Name: "base", 248 | B: Candidate{ 249 | Base: Addr{ 250 | IP: net.IPv4(127, 0, 0, 1), 251 | }, 252 | }, 253 | Equal: false, 254 | }, 255 | { 256 | Name: "related", 257 | B: Candidate{ 258 | Related: Addr{ 259 | IP: net.IPv4(127, 0, 0, 1), 260 | }, 261 | }, 262 | Equal: false, 263 | }, 264 | { 265 | Name: "componentID", 266 | B: Candidate{ 267 | ComponentID: 10, 268 | }, 269 | Equal: false, 270 | }, 271 | } { 272 | t.Run(tc.Name, func(t *testing.T) { 273 | if v := tc.A.Equal(&tc.B); v != tc.Equal { 274 | t.Error("not equal") 275 | } 276 | }) 277 | } 278 | } 279 | 280 | func TestCandidatesSort(t *testing.T) { 281 | candidates := Candidates{ 282 | {Priority: 1}, {Priority: 3}, {Priority: 2}, 283 | } 284 | sort.Sort(candidates) 285 | for i, p := range []int{3, 2, 1} { 286 | if candidates[i].Priority != p { 287 | t.Errorf("p[%d] %d (got) != %d (expected)", i, 288 | candidates[i].Priority, p, 289 | ) 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /host_test.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "gortc.io/ice/gather" 8 | ) 9 | 10 | func TestProcessDualStack(t *testing.T) { 11 | const maxCount = 100 12 | tt := make([]struct { 13 | V4, V6 int 14 | }, 0, maxCount*maxCount) 15 | // Not checking v4=0 and v6=0 because that case is invalid for 16 | // the processDualStack function. 17 | for v4 := 1; v4 <= maxCount; v4++ { 18 | for v6 := 1; v6 <= maxCount; v6++ { 19 | tt = append(tt, struct{ V4, V6 int }{V4: v4, V6: v6}) 20 | } 21 | } 22 | for _, tc := range tt { 23 | var v4, v6, all []gather.Addr 24 | for i := 0; i < tc.V4; i++ { 25 | a := gather.Addr{ 26 | IP: make(net.IP, net.IPv4len), 27 | } 28 | // "marking" IP so we can count unique ip's. 29 | bin.PutUint32(a.IP, uint32(i)) 30 | v4 = append(v4, a) 31 | all = append(all, a) 32 | } 33 | for i := 0; i < tc.V6; i++ { 34 | a := gather.Addr{ 35 | IP: make(net.IP, net.IPv6len), 36 | } 37 | bin.PutUint32(a.IP, uint32(i)) 38 | v6 = append(v6, a) 39 | all = append(all, a) 40 | } 41 | // Checking that output length is equal to total length. 42 | result := processDualStack(all, v4, v6) 43 | if len(result) != len(all) { 44 | t.Errorf("v4: %d, v6: %d: expected %d, got %d", tc.V4, tc.V6, len(all), len(result)) 45 | } 46 | // Checking unique IP count. 47 | gotV4 := make(map[uint32]bool) 48 | gotV6 := make(map[uint32]bool) 49 | for _, r := range result { 50 | if r.IP.To4() == nil { 51 | gotV6[bin.Uint32(r.IP)] = true 52 | } else { 53 | gotV4[bin.Uint32(r.IP)] = true 54 | } 55 | } 56 | if len(gotV4) != len(v4) { 57 | t.Errorf("v4: %d, v6: %d: v4 expected %d, got %d", tc.V4, tc.V6, len(v4), len(gotV4)) 58 | } 59 | if len(gotV6) != len(v6) { 60 | t.Errorf("v4: %d, v6: %d: v6 expected %d, got %d", tc.V4, tc.V6, len(v6), len(gotV6)) 61 | } 62 | } 63 | } 64 | 65 | func TestGatherHostAddresses(t *testing.T) { 66 | type outputRow struct { 67 | IP string 68 | Preference int 69 | } 70 | for _, tc := range []struct { 71 | Name string 72 | Input []string 73 | Output []outputRow 74 | }{ 75 | { 76 | Name: "blank", 77 | }, 78 | { 79 | Name: "loopback", 80 | Input: []string{ 81 | "127.0.0.1", 82 | }, 83 | }, 84 | { 85 | Name: "Single IPv4", 86 | Input: []string{ 87 | "1.1.1.1", 88 | }, 89 | Output: []outputRow{ 90 | {"1.1.1.1", 65535}, 91 | }, 92 | }, 93 | { 94 | Name: "IPv4", 95 | Input: []string{ 96 | "1.1.1.1", 97 | "1.1.1.2", 98 | }, 99 | Output: []outputRow{ 100 | {"1.1.1.1", 2}, 101 | {"1.1.1.2", 1}, 102 | }, 103 | }, 104 | { 105 | Name: "Single IPv6", 106 | Input: []string{ 107 | "2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa1", 108 | }, 109 | Output: []outputRow{ 110 | {"2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa1", 65535}, 111 | }, 112 | }, 113 | { 114 | Name: "IPv6", 115 | Input: []string{ 116 | "2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa1", 117 | "2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa2", 118 | }, 119 | Output: []outputRow{ 120 | {"2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa1", 2}, 121 | {"2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa2", 1}, 122 | }, 123 | }, 124 | { 125 | // If a host has two IPv4 addresses and six IPv6 addresses, it will 126 | // insert an IPv4 address after four IPv6 addresses by choosing the 127 | // appropriate local preference values when calculating the pair 128 | // priorities. 129 | Name: "2xIPv4 and 6xIPv6", 130 | Input: []string{ 131 | "2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa1", 132 | "2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa2", 133 | "2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa3", 134 | "2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa4", 135 | "2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa5", 136 | "2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa6", 137 | "1.1.1.1", 138 | "1.1.1.2", 139 | }, 140 | Output: []outputRow{ 141 | {"2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa1", 8}, 142 | {"2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa2", 7}, 143 | {"2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa3", 6}, 144 | {"2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa4", 5}, 145 | {"1.1.1.1", 4}, 146 | {"2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa5", 3}, 147 | {"2a03:e2c0:60f:52:cfe1:fdd:daf7:7fa6", 2}, 148 | {"1.1.1.2", 1}, 149 | }, 150 | }, 151 | } { 152 | t.Run(tc.Name, func(t *testing.T) { 153 | gatherAddrs := make([]gather.Addr, len(tc.Input)) 154 | for i, ip := range tc.Input { 155 | gatherAddrs[i] = gather.Addr{ 156 | IP: net.ParseIP(ip), 157 | } 158 | } 159 | expected := make([]HostAddr, len(tc.Output)) 160 | for i, row := range tc.Output { 161 | expected[i] = HostAddr{ 162 | IP: net.ParseIP(row.IP), 163 | LocalPreference: row.Preference, 164 | } 165 | } 166 | gotAddr, err := HostAddresses(gatherAddrs) 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | if len(gotAddr) != len(expected) { 171 | t.Fatalf("bad length: %d (got) != %d (expected)", 172 | len(gotAddr), len(expected), 173 | ) 174 | } 175 | for i := range gotAddr { 176 | got := gotAddr[i] 177 | exp := expected[i] 178 | if got.LocalPreference != exp.LocalPreference || !got.IP.Equal(exp.IP) { 179 | t.Errorf("[%d]: %s, %d (got) != %s, %d (expected)", 180 | i, got.IP, got.LocalPreference, exp.IP, exp.LocalPreference, 181 | ) 182 | } 183 | } 184 | }) 185 | } 186 | } 187 | 188 | func TestIsValidHostIP(t *testing.T) { 189 | for _, tc := range []struct { 190 | Name string 191 | IP net.IP 192 | V6 bool 193 | Valid bool 194 | }{ 195 | { 196 | Name: "blank", 197 | }, 198 | { 199 | Name: "127.0.0.1", 200 | IP: localIP, 201 | }, 202 | { 203 | Name: "v4", 204 | IP: net.IPv4(10, 0, 0, 1), 205 | Valid: true, 206 | }, 207 | { 208 | Name: "v4 for v6 only", 209 | IP: net.IPv4(10, 0, 0, 1), 210 | V6: true, 211 | }, 212 | { 213 | Name: "Site-local ipv6", 214 | IP: net.ParseIP("FEC0::ff:aa"), 215 | V6: true, 216 | }, 217 | { 218 | Name: "link-local ipv6", 219 | IP: net.ParseIP("fe80::50da:9baa:ef96:15c8"), 220 | }, 221 | { 222 | Name: "ipv4-mapped", 223 | IP: net.IPv4(10, 0, 0, 1).To16(), 224 | Valid: true, 225 | }, 226 | { 227 | Name: "ipv4-mapped for v6 only", 228 | IP: net.IPv4(10, 0, 0, 1).To16(), 229 | V6: true, 230 | }, 231 | } { 232 | t.Run(tc.Name, func(t *testing.T) { 233 | if v := IsHostIPValid(tc.IP, tc.V6); v != tc.Valid { 234 | t.Errorf("valid(%s, v6=%v) %v (got) != %v (expected)", 235 | tc.IP, tc.V6, v, tc.Valid, 236 | ) 237 | } 238 | }) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /pair.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | func min(a, b int64) int64 { 10 | if a < b { 11 | return a 12 | } 13 | return b 14 | } 15 | 16 | func max(a, b int64) int64 { 17 | if a > b { 18 | return a 19 | } 20 | return b 21 | } 22 | 23 | // PairPriority computes Pair Priority as in RFC 8445 Section 6.1.2.3. 24 | func PairPriority(controlling, controlled int) int64 { 25 | var ( 26 | g = int64(controlling) 27 | d = int64(controlled) 28 | ) 29 | // pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0) 30 | v := (1<<32)*min(g, d) + 2*max(g, d) 31 | if g > d { 32 | v++ 33 | } 34 | return v 35 | } 36 | 37 | // Pair wraps two candidates, one is local, other is remote. 38 | type Pair struct { 39 | Local Candidate `json:"local"` 40 | Remote Candidate `json:"remote"` 41 | Priority int64 `json:"priority"` 42 | Foundation []byte `json:"foundation"` 43 | State PairState `json:"state"` 44 | Nominated bool `json:"nominated"` 45 | ComponentID int `json:"component_id"` 46 | } 47 | 48 | // Equal returns true if pair p equals to pair b. 49 | func (p *Pair) Equal(b *Pair) bool { 50 | if p.ComponentID != b.ComponentID { 51 | return false 52 | } 53 | if p.Nominated != b.Nominated { 54 | return false 55 | } 56 | if p.State != b.State { 57 | return false 58 | } 59 | if p.Priority != b.Priority { 60 | return false 61 | } 62 | if !p.Local.Equal(&b.Local) { 63 | return false 64 | } 65 | if !p.Remote.Equal(&b.Remote) { 66 | return false 67 | } 68 | if !bytes.Equal(p.Foundation, b.Foundation) { 69 | return false 70 | } 71 | return true 72 | } 73 | 74 | // PairState as defined in RFC 8445 Section 6.1.2.6. 75 | type PairState byte 76 | 77 | // In returns true if s in states list. 78 | func (s PairState) In(states ...PairState) bool { 79 | for _, st := range states { 80 | if st == s { 81 | return true 82 | } 83 | } 84 | return false 85 | } 86 | 87 | // UnmarshalText implements TextUnmarshaler. 88 | func (s *PairState) UnmarshalText(text []byte) error { 89 | for k, v := range pairStateToStr { 90 | if string(text) == v { 91 | *s = k 92 | return nil 93 | } 94 | } 95 | return fmt.Errorf("unknown pair state value %q", text) 96 | } 97 | 98 | // MarshalText implements TextMarshaler. 99 | func (s PairState) MarshalText() ([]byte, error) { 100 | return []byte(s.String()), nil 101 | } 102 | 103 | var pairStateToStr = map[PairState]string{ 104 | PairWaiting: "Waiting", 105 | PairInProgress: "In-Progress", 106 | PairSucceeded: "Succeeded", 107 | PairFailed: "Failed", 108 | PairFrozen: "Frozen", 109 | } 110 | 111 | func (s PairState) String() string { return pairStateToStr[s] } 112 | 113 | const ( 114 | // PairFrozen state: A check for this pair has not been sent, and it cannot 115 | // be sent until the pair is unfrozen and moved into the Waiting state. 116 | PairFrozen PairState = iota 117 | // PairInProgress state: A check has been sent for this pair, but the 118 | // transaction is in progress. 119 | PairInProgress 120 | // PairSucceeded state: A check has been sent for this pair, and it produced 121 | // a successful result. 122 | PairSucceeded 123 | // PairFailed state: A check has been sent for this pair, and it failed (a 124 | // response to the check was never received, or a failure response was 125 | // received). 126 | PairFailed 127 | // PairWaiting state: A check has not been sent for this pair, but the pair 128 | // is not Frozen. 129 | PairWaiting 130 | ) 131 | 132 | // SetFoundation sets foundation, the combination of candidates foundations. 133 | func (p *Pair) SetFoundation() { 134 | f := make([]byte, foundationLength*2) 135 | copy(f[:foundationLength], p.Local.Foundation) 136 | copy(f[foundationLength:], p.Remote.Foundation) 137 | p.Foundation = f 138 | } 139 | 140 | // SetPriority calculates and sets pair priority based on role and candidates. 141 | func (p *Pair) SetPriority(role Role) { 142 | var ( 143 | controlling = p.Local.Priority 144 | controlled = p.Remote.Priority 145 | ) 146 | if role == Controlled { 147 | controlling, controlled = controlled, controlling 148 | } 149 | p.Priority = PairPriority(controlling, controlled) 150 | } 151 | 152 | // Pairs is ordered slice of Pair elements. 153 | type Pairs []Pair 154 | 155 | func (p Pairs) Len() int { return len(p) } 156 | 157 | func (p Pairs) Less(i, j int) bool { 158 | if p[i].Priority == p[j].Priority { 159 | return p[i].ComponentID < p[j].ComponentID 160 | } 161 | return p[i].Priority > p[j].Priority 162 | } 163 | 164 | func (p Pairs) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 165 | 166 | func sameFamily(a, b net.IP) bool { 167 | return len(a.To4()) == len(b.To4()) 168 | } 169 | 170 | // NewPairs pairs each local candidate with each remote candidate for the same 171 | // component of the same data stream with the same IP address family. Candidates 172 | // should be sorted by priority in descending order, which is default order for 173 | // the Candidates type. Populates only Local, Remote and ComponentID fields of Pair. 174 | // 175 | // See RFC 8445 Section 6.1.2.2. 176 | func NewPairs(local, remote Candidates) Pairs { 177 | p := make(Pairs, 0, 100) 178 | for l := range local { 179 | for r := range remote { 180 | // Same data stream. 181 | if local[l].ComponentID != remote[r].ComponentID { 182 | continue 183 | } 184 | ipL, ipR := local[l].Addr.IP, remote[r].Addr.IP 185 | // Same IP address family. 186 | if !sameFamily(ipL, ipR) { 187 | continue 188 | } 189 | if ipL.To4() == nil && ipL.IsLinkLocalUnicast() { 190 | // IPv6 link-local addresses MUST NOT be paired with other 191 | // than link-local addresses. 192 | if !ipR.IsLinkLocalUnicast() { 193 | continue 194 | } 195 | } 196 | pair := Pair{ 197 | Local: local[l], 198 | Remote: remote[r], 199 | ComponentID: local[l].ComponentID, 200 | } 201 | pair.SetFoundation() 202 | p = append(p, pair) 203 | } 204 | } 205 | return p 206 | } 207 | 208 | const maxPairFoundationBytes = 64 209 | 210 | type foundationKey [maxPairFoundationBytes]byte 211 | 212 | type foundationSet map[foundationKey]struct{} 213 | 214 | func getFoundationKey(f []byte) foundationKey { 215 | k := foundationKey{} 216 | copy(k[:], f) 217 | return k 218 | } 219 | 220 | func assertFoundationLength(f []byte) { 221 | if len(f) > maxPairFoundationBytes { 222 | panic("length of foundation is greater that maximum") 223 | } 224 | } 225 | 226 | func (s foundationSet) Contains(f []byte) bool { 227 | assertFoundationLength(f) 228 | _, ok := s[getFoundationKey(f)] 229 | return ok 230 | } 231 | 232 | func (s foundationSet) Add(f []byte) { 233 | assertFoundationLength(f) 234 | s[getFoundationKey(f)] = struct{}{} 235 | } 236 | 237 | type pairKey struct { 238 | LocalIP [net.IPv6len]byte 239 | RemoteIP [net.IPv6len]byte 240 | LocalPort int 241 | RemotePort int 242 | } 243 | 244 | func (k pairKey) Equal(p *Pair) bool { 245 | b := getPairKey(p) 246 | return k == b 247 | } 248 | 249 | func getPairKey(p *Pair) pairKey { 250 | k := pairKey{} 251 | copy(k.LocalIP[:], p.Local.Addr.IP) 252 | copy(k.RemoteIP[:], p.Remote.Addr.IP) 253 | k.LocalPort = p.Local.Addr.Port 254 | k.RemotePort = p.Remote.Addr.Port 255 | return k 256 | } 257 | -------------------------------------------------------------------------------- /agent_gather.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "strconv" 7 | "sync" 8 | 9 | "go.uber.org/zap" 10 | 11 | "gortc.io/stun" 12 | "gortc.io/turn" 13 | "gortc.io/turnc" 14 | ) 15 | 16 | func withGatherer(g candidateGatherer) AgentOption { 17 | return func(a *Agent) error { 18 | a.gatherer = g 19 | return nil 20 | } 21 | } 22 | 23 | type gathererOptions struct { 24 | Components int 25 | IPv4Only bool 26 | } 27 | 28 | type candidateGatherer interface { 29 | gatherUDP(opt gathererOptions) ([]*localUDPCandidate, error) 30 | } 31 | 32 | func (c *localUDPCandidate) Close() error { 33 | return c.conn.Close() 34 | } 35 | 36 | func (c *localUDPCandidate) readUntilClose(a *Agent) { 37 | for { 38 | buf := make([]byte, 1024) 39 | n, addr, err := c.conn.ReadFrom(buf) 40 | if err != nil { 41 | break 42 | } 43 | udpAddr, ok := addr.(*net.UDPAddr) 44 | if !ok { 45 | break 46 | } 47 | c.mux.Lock() 48 | var pipe localPipe 49 | for _, p := range c.pipes { 50 | if !p.addr.IP.Equal(udpAddr.IP) { 51 | continue 52 | } 53 | if p.addr.Port != udpAddr.Port { 54 | continue 55 | } 56 | pipe = p 57 | } 58 | c.mux.Unlock() 59 | if pipe.addr != nil { 60 | _, err = pipe.conn.Write(buf[:n]) 61 | if err != nil && err != io.ErrClosedPipe { 62 | c.log.Debug("pipe write failed", zap.Error(err)) 63 | } else { 64 | continue 65 | } 66 | } 67 | go func() { 68 | if err := a.processUDP(buf[:n], c, udpAddr); err != nil { 69 | c.log.Error("processUDP failed", zap.Error(err)) 70 | } else { 71 | c.log.Debug("processed") 72 | } 73 | }() 74 | } 75 | } 76 | 77 | // GatherCandidatesForStream allows gathering candidates for multiple streams. 78 | // The streamID is integer that starts from zero. 79 | func (a *Agent) GatherCandidatesForStream(streamID int) error { 80 | if len(a.localCandidates) > streamID { 81 | return errStreamAlreadyExist 82 | } 83 | candidates, err := a.gatherer.gatherUDP(gathererOptions{Components: 1, IPv4Only: a.ipv4Only}) 84 | if err != nil { 85 | return err 86 | } 87 | a.localCandidates = append(a.localCandidates, candidates) 88 | for i := range candidates { 89 | candidates[i].log = a.log.Named("candidate").With( 90 | zap.Stringer("addr", candidates[i].candidate.Addr), 91 | ) 92 | go candidates[i].readUntilClose(a) 93 | } 94 | if len(a.stun) > 0 { 95 | if err = a.gatherServerReflexiveCandidatesFor(streamID); err != nil { 96 | return err 97 | } 98 | } 99 | if len(a.turn) > 0 { 100 | if err = a.gatherRelayedCandidatesFor(streamID); err != nil { 101 | return err 102 | } 103 | } 104 | return nil 105 | } 106 | 107 | func resolveSTUN(uri stun.URI) (*net.UDPAddr, error) { 108 | if uri.Port == 0 { 109 | uri.Port = stun.DefaultPort 110 | } 111 | hostPort := net.JoinHostPort(uri.Host, strconv.Itoa(uri.Port)) 112 | addr, err := net.ResolveUDPAddr("udp", hostPort) 113 | return addr, err 114 | } 115 | 116 | func resolveTURN(uri turn.URI) (*net.UDPAddr, error) { 117 | if uri.Port == 0 { 118 | uri.Port = turn.DefaultPort 119 | } 120 | hostPort := net.JoinHostPort(uri.Host, strconv.Itoa(uri.Port)) 121 | addr, err := net.ResolveUDPAddr("udp", hostPort) 122 | return addr, err 123 | } 124 | 125 | func (a *Agent) gatherServerReflexiveCandidates(log *zap.Logger, c *localUDPCandidate, s stunServerOptions) error { 126 | addr, err := resolveSTUN(s.uri) 127 | if err != nil { 128 | return err 129 | } 130 | // TODO: Setup correct RTO. 131 | client, err := stun.NewClient(c.Pipe(addr), stun.WithRTO(a.ta/2)) 132 | if err != nil { 133 | return err 134 | } 135 | var bindErr error 136 | if doErr := client.Do(stun.MustBuild(stun.TransactionID, stun.BindingRequest, stun.Fingerprint), func(event stun.Event) { 137 | if event.Error != nil { 138 | bindErr = event.Error 139 | return 140 | } 141 | var mappedAddr stun.XORMappedAddress 142 | if getErr := mappedAddr.GetFrom(event.Message); getErr != nil { 143 | bindErr = getErr 144 | } 145 | log.Debug("got server reflexive candidate", zap.Stringer("addr", mappedAddr)) 146 | }); doErr != nil { 147 | return doErr 148 | } 149 | if err = client.Close(); err != nil { 150 | return err 151 | } 152 | if bindErr != nil { 153 | log.Debug("binding error", zap.Error(bindErr)) 154 | } 155 | return nil 156 | } 157 | 158 | func (a *Agent) gatherServerReflexiveCandidatesFor(streamID int) error { 159 | localCandidates := a.localCandidates[streamID] 160 | for _, c := range localCandidates { 161 | if c.candidate.Addr.IP.To4() == nil { 162 | continue 163 | } 164 | for _, s := range a.stun { 165 | log := a.log.With( 166 | zap.Stringer("uri", s.uri), 167 | zap.Stringer("addr", c.candidate.Addr), 168 | ) 169 | log.Debug("gathering server-reflexive candidates") 170 | if err := a.gatherServerReflexiveCandidates(log, c, s); err != nil { 171 | log.Error("failed to gather server reflexive candidates") 172 | } 173 | } 174 | } 175 | return nil 176 | } 177 | 178 | func (a *Agent) gatherRelayedCandidates(log *zap.Logger, c *localUDPCandidate, s turnServerOptions) error { 179 | addr, err := resolveTURN(s.uri) 180 | if err != nil { 181 | return err 182 | } 183 | client, err := turnc.New(turnc.Options{ 184 | Conn: c.Pipe(addr), 185 | Username: s.username, 186 | Password: s.password, 187 | Log: a.log.Named("turn").With(zap.Stringer("local", c.candidate.Addr)), 188 | RTO: a.ta / 2, // TODO: setup correct RTO 189 | }) 190 | if err != nil { 191 | return err 192 | } 193 | alloc, err := client.Allocate() 194 | if err != nil { 195 | log.Warn("failed to allocate", zap.Error(err)) 196 | return nil 197 | } 198 | c.mux.Lock() 199 | c.alloc = alloc 200 | c.mux.Unlock() 201 | log.Debug("turn allocated") 202 | return nil 203 | } 204 | 205 | func (a *Agent) gatherRelayedCandidatesFor(streamID int) error { 206 | localCandidates := a.localCandidates[streamID] 207 | for _, c := range localCandidates { 208 | if c.candidate.Addr.IP.To4() == nil { 209 | continue 210 | } 211 | for _, s := range a.turn { 212 | log := a.log.With( 213 | zap.Stringer("uri", s.uri), 214 | zap.Stringer("addr", c.candidate.Addr), 215 | ) 216 | log.Debug("gathering relayed candidates") 217 | if err := a.gatherRelayedCandidates(log, c, s); err != nil { 218 | log.Error("failed to gather relayed candidates") 219 | } 220 | } 221 | } 222 | return nil 223 | } 224 | 225 | type localUDPCandidate struct { 226 | log *zap.Logger 227 | candidate Candidate 228 | conn net.PacketConn 229 | stream int 230 | alloc *turnc.Allocation 231 | 232 | pipes []localPipe 233 | mux sync.Mutex 234 | } 235 | 236 | // Pipe returns a connection to addr from local candidate. 237 | func (c *localUDPCandidate) Pipe(addr *net.UDPAddr) net.Conn { 238 | lconn, rconn := net.Pipe() 239 | c.mux.Lock() 240 | c.pipes = append(c.pipes, localPipe{ 241 | addr: addr, 242 | conn: rconn, 243 | }) 244 | c.mux.Unlock() 245 | go func() { 246 | for { 247 | buf := make([]byte, 1024) 248 | n, readErr := rconn.Read(buf) 249 | if readErr != nil { 250 | break 251 | } 252 | _, writeErr := c.conn.WriteTo(buf[:n], addr) 253 | if writeErr != nil { 254 | c.log.Debug("WriteTo failed", zap.Error(writeErr)) 255 | _ = rconn.Close() 256 | } 257 | } 258 | }() 259 | return lconn 260 | } 261 | 262 | type localPipe struct { 263 | addr *net.UDPAddr 264 | conn net.Conn 265 | } 266 | -------------------------------------------------------------------------------- /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/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 8 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 9 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 10 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 15 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 17 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 18 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 22 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 23 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 24 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 25 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 26 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 27 | go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= 28 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 29 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 30 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 31 | go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= 32 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 33 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 34 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 35 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 36 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 37 | go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= 38 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 39 | go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo= 40 | go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 41 | go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM= 42 | go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 43 | go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 44 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 45 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 46 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 47 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 48 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 49 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 50 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 51 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 52 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 53 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 56 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 57 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 58 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 59 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= 60 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 61 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 62 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 63 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 64 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 66 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 67 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 68 | gortc.io/sdp v0.18.0 h1:1cxeCC2Bv7eUD9d5YrrCbYO8uEGQGvz86AKdAyaJVZk= 69 | gortc.io/sdp v0.18.0/go.mod h1:iwG4LtzGX1MLglrl5x7AL7cfLndFPMfJ02koQl5KIgE= 70 | gortc.io/sdp v0.18.2/go.mod h1:Oj8tpRIx+Zx6lyrQR9+HHegByfbaz92A9wPSWrQhTI4= 71 | gortc.io/stun v1.21.0 h1:hFUdDaQAlnRcrPcUhwPkV+XJWrY1shDjAPG4f8vu5Hw= 72 | gortc.io/stun v1.21.0/go.mod h1:XD5lpONVyjvV3BgOyJFNo0iv6R2oZB4L+weMqxts+zg= 73 | gortc.io/stun v1.22.0 h1:JUH+hPON8oOUcE0Hwoj/QJCY/uRtxBGD2qPL7yit1NY= 74 | gortc.io/stun v1.22.0/go.mod h1:XD5lpONVyjvV3BgOyJFNo0iv6R2oZB4L+weMqxts+zg= 75 | gortc.io/stun v1.22.1 h1:96mOdDATYRqhYB+TZdenWBg4CzL2Ye5kPyBXQ8KAB+8= 76 | gortc.io/stun v1.22.1/go.mod h1:XD5lpONVyjvV3BgOyJFNo0iv6R2oZB4L+weMqxts+zg= 77 | gortc.io/stun v1.23.0/go.mod h1:XD5lpONVyjvV3BgOyJFNo0iv6R2oZB4L+weMqxts+zg= 78 | gortc.io/turn v0.10.0 h1:K+xLxzvTFqIRQCfixAZUMRChmmasZAdn7yJNDl22E+s= 79 | gortc.io/turn v0.10.0/go.mod h1:C5yO0WYgZj1fTGRKRJVyotcdDDdAUyg5MTI7re5cHuI= 80 | gortc.io/turn v0.11.2 h1:YpusvCCdGbfhul7SeYE4exeokxdf6qe3jm9cqL7coL8= 81 | gortc.io/turn v0.11.2/go.mod h1:OSr51+P6AiA9ZXTG4kI8WPsfkacTjvgCIYpx0ODGiVw= 82 | gortc.io/turnc v0.2.0 h1:Gj1GTDE0dUuizYEGN8Hs0fGDJBQetqRsTeLMuzykrKg= 83 | gortc.io/turnc v0.2.0/go.mod h1:Y27swRjXbt4a6VJGsK0EzX6uebwygFymdT6tFuuyAvc= 84 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 85 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 86 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/chromedp/cdproto v0.0.0-20180713053126-e314dc107013/go.mod h1:C2GPAraqdt1KfZU7aSmx1XUgarNq/3JmxevQkmCjOVs= 3 | github.com/chromedp/cdproto v0.0.0-20180731224315-b8925c84f3c4 h1:KXkQywT4MsPpnEI8Yih8D/xojLGmUtVcGxMMwqIwnYg= 4 | github.com/chromedp/cdproto v0.0.0-20180731224315-b8925c84f3c4/go.mod h1:C2GPAraqdt1KfZU7aSmx1XUgarNq/3JmxevQkmCjOVs= 5 | github.com/chromedp/chromedp v0.1.2 h1:qB/dpbbbOPGkKyZU2gKB49jp+ZvY9C3rPUfYELLz+6g= 6 | github.com/chromedp/chromedp v0.1.2/go.mod h1:83UDY5CKmHrvKLQ6vVU+LVFUcfjOSPNufx8XFWLUYlQ= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/disintegration/imaging v1.4.2 h1:BSVxoYQ2NfLdvIGCDD8GHgBV5K0FCEsc0d/6FxQII3I= 11 | github.com/disintegration/imaging v1.4.2/go.mod h1:9B/deIUIrliYkyMTuXJd6OUFLcrZ2tf+3Qlwnaf/CjU= 12 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 13 | github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= 14 | github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 15 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 16 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 17 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 18 | github.com/knq/sysutil v0.0.0-20180306023629-0218e141a794 h1:hgWKTlyruPI7k8W+0FmTMLf+8d2KPxyzTxsfDDQhNp8= 19 | github.com/knq/sysutil v0.0.0-20180306023629-0218e141a794/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= 20 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 21 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 22 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 23 | github.com/mailru/easyjson v0.0.0-20180606163543-3fdea8d05856/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 24 | github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5 h1:0x4qcEHDpruK6ML/m/YSlFUUu0UpRD3I2PHsNCuGnyA= 25 | github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 26 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 27 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 28 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 34 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 35 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 36 | go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= 37 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 38 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 39 | go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= 40 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 41 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 42 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 43 | go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= 44 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 45 | go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 46 | go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 47 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 48 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 49 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI= 50 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 51 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 52 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 53 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 54 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I= 55 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 56 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 57 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 58 | golang.org/x/net v0.0.0-20190926025831-c00fd9afed17 h1:qPnAdmjNA41t3QBTx2mFGf/SD1IoslhYu7AmdsVzCcs= 59 | golang.org/x/net v0.0.0-20190926025831-c00fd9afed17/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 60 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 61 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 62 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 64 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 65 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 66 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 67 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 68 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 69 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 70 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 71 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 72 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 73 | gortc.io/sdp v0.17.0 h1:gPmGXqyszHplnlMeF2X0eCOK+G8aAK4PP6puglIcofc= 74 | gortc.io/sdp v0.17.0/go.mod h1:iwG4LtzGX1MLglrl5x7AL7cfLndFPMfJ02koQl5KIgE= 75 | gortc.io/sdp v0.18.0/go.mod h1:iwG4LtzGX1MLglrl5x7AL7cfLndFPMfJ02koQl5KIgE= 76 | gortc.io/stun v1.21.0 h1:hFUdDaQAlnRcrPcUhwPkV+XJWrY1shDjAPG4f8vu5Hw= 77 | gortc.io/stun v1.21.0/go.mod h1:XD5lpONVyjvV3BgOyJFNo0iv6R2oZB4L+weMqxts+zg= 78 | gortc.io/stun v1.21.1 h1:r7t/G5MIBWjsKbs7CTXK4DWjuaaJhuWYTbcL0xMlF9U= 79 | gortc.io/stun v1.21.1/go.mod h1:XD5lpONVyjvV3BgOyJFNo0iv6R2oZB4L+weMqxts+zg= 80 | gortc.io/stun v1.22.0/go.mod h1:XD5lpONVyjvV3BgOyJFNo0iv6R2oZB4L+weMqxts+zg= 81 | gortc.io/stun v1.22.1/go.mod h1:XD5lpONVyjvV3BgOyJFNo0iv6R2oZB4L+weMqxts+zg= 82 | gortc.io/turn v0.10.0 h1:K+xLxzvTFqIRQCfixAZUMRChmmasZAdn7yJNDl22E+s= 83 | gortc.io/turn v0.10.0/go.mod h1:C5yO0WYgZj1fTGRKRJVyotcdDDdAUyg5MTI7re5cHuI= 84 | gortc.io/turn v0.11.2/go.mod h1:OSr51+P6AiA9ZXTG4kI8WPsfkacTjvgCIYpx0ODGiVw= 85 | gortc.io/turnc v0.1.2 h1:jYXcgfYoddKVKsAaLhW9XPqpaVAfovQjwSPfhBXb2DI= 86 | gortc.io/turnc v0.1.2/go.mod h1:Y27swRjXbt4a6VJGsK0EzX6uebwygFymdT6tFuuyAvc= 87 | gortc.io/turnc v0.2.0 h1:Gj1GTDE0dUuizYEGN8Hs0fGDJBQetqRsTeLMuzykrKg= 88 | gortc.io/turnc v0.2.0/go.mod h1:Y27swRjXbt4a6VJGsK0EzX6uebwygFymdT6tFuuyAvc= 89 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 90 | -------------------------------------------------------------------------------- /agent_binding.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "time" 9 | 10 | "go.uber.org/zap" 11 | 12 | "gortc.io/ice/candidate" 13 | "gortc.io/stun" 14 | ) 15 | 16 | func (a *Agent) handleBindingRequest(m *stun.Message, c *localUDPCandidate, raddr Addr) error { 17 | a.log.Debug("handling binding request", 18 | zap.Stringer("remote", raddr), 19 | zap.Stringer("local", c.candidate.Addr), 20 | ) 21 | if err := stun.Fingerprint.Check(m); err != nil { 22 | return err 23 | } 24 | integrity := stun.NewShortTermIntegrity(a.localPassword) 25 | if err := integrity.Check(m); err != nil { 26 | return err 27 | } 28 | remoteCandidate, ok := a.remoteCandidateByAddr(raddr) 29 | if !ok { 30 | return errCandidateNotFound 31 | } 32 | pair := Pair{ 33 | Local: c.candidate, 34 | Remote: remoteCandidate, 35 | } 36 | pair.SetFoundation() 37 | pair.SetPriority(a.role) 38 | 39 | a.mux.Lock() 40 | defer a.mux.Unlock() 41 | list := a.set[c.stream] 42 | 43 | for i := range list.Pairs { 44 | if !list.Pairs[i].Local.Equal(&pair.Local) { 45 | continue 46 | } 47 | if !list.Pairs[i].Remote.Equal(&pair.Remote) { 48 | continue 49 | } 50 | state := list.Pairs[i].State 51 | a.log.Debug("found", zap.Stringer("state", state)) 52 | pair.State = PairWaiting 53 | list.Triggered = append(list.Triggered, list.Pairs[i]) 54 | a.set[c.stream] = list 55 | a.log.Debug("added to triggered set", 56 | zap.Stringer("local", pair.Local.Addr), 57 | zap.Stringer("remote", pair.Remote.Addr), 58 | ) 59 | // Sending response. 60 | res := stun.MustBuild(m, stun.BindingSuccess, 61 | &stun.XORMappedAddress{ 62 | IP: raddr.IP, 63 | Port: raddr.Port, 64 | }, 65 | integrity, stun.Fingerprint, 66 | ) 67 | a.log.Debug("writing", zap.Stringer("m", res)) 68 | _, err := c.conn.WriteTo(res.Raw, &net.UDPAddr{ 69 | Port: raddr.Port, 70 | IP: raddr.IP, 71 | }) 72 | if err == nil { 73 | a.log.Debug("wrote response", zap.Stringer("m", res)) 74 | } else { 75 | a.log.Debug("write err", zap.Error(err)) 76 | } 77 | return err 78 | } 79 | 80 | list.Pairs = append(list.Pairs, pair) 81 | list.Sort() 82 | a.set[c.stream] = list 83 | return nil 84 | } 85 | 86 | func (a *Agent) handleBindingResponse(t *agentTransaction, p *Pair, m *stun.Message, raddr Addr) error { 87 | if err := a.processBindingResponse(t, p, m, raddr); err != nil { 88 | // TODO: Handle nomination failure. 89 | 90 | a.mux.Lock() 91 | a.setPairStateByKey(t.checklist, t.pair, PairFailed) 92 | a.mux.Unlock() 93 | 94 | a.log.Debug("response process failed", zap.Error(err), 95 | zap.Stringer("remote", p.Remote.Addr), 96 | zap.Stringer("local", p.Local.Addr), 97 | ) 98 | return err 99 | } 100 | 101 | a.mux.Lock() 102 | a.setPairStateByKey(t.checklist, t.pair, PairSucceeded) 103 | a.mux.Unlock() 104 | 105 | a.log.Debug("response succeeded", 106 | zap.Stringer("remote", p.Remote.Addr), 107 | zap.Stringer("local", p.Local.Addr), 108 | ) 109 | // Adding to valid list. 110 | // TODO: Construct valid pair as in https://tools.ietf.org/html/rfc8445#section-7.2.5.3.2 111 | // Handling case "1" only, when valid pair is equal to generated pair p. 112 | validPair := *p 113 | a.mux.Lock() 114 | cl := a.set[t.checklist] 115 | 116 | // Setting all candidate paris with same foundation to "Waiting". 117 | for cID, c := range a.set { 118 | for i := range c.Pairs { 119 | if samePair(p, &c.Pairs[i]) { 120 | continue 121 | } 122 | if bytes.Equal(c.Pairs[i].Foundation, p.Foundation) { 123 | a.setPairState(cID, i, PairWaiting) 124 | continue 125 | } 126 | if bytes.Equal(c.Pairs[i].Foundation, validPair.Foundation) { 127 | a.setPairState(cID, i, PairWaiting) 128 | } 129 | } 130 | } 131 | 132 | // Nominating. 133 | if t.nominate { 134 | validPair.Nominated = true 135 | } 136 | a.log.Debug("added to valid list", 137 | zap.Stringer("local", validPair.Local.Addr), 138 | zap.Stringer("remote", validPair.Remote.Addr), 139 | ) 140 | found := false 141 | for i := range cl.Valid { 142 | if cl.Valid[i].ComponentID != validPair.ComponentID { 143 | continue 144 | } 145 | if !cl.Valid[i].Remote.Addr.Equal(validPair.Remote.Addr) { 146 | continue 147 | } 148 | if !cl.Valid[i].Local.Addr.Equal(validPair.Local.Addr) { 149 | continue 150 | } 151 | a.log.Debug("nominating", 152 | zap.Stringer("remote", validPair.Remote.Addr), 153 | zap.Stringer("local", validPair.Local.Addr), 154 | ) 155 | found = true 156 | cl.Valid[i].Nominated = true 157 | } 158 | if !found { 159 | cl.Valid = append(cl.Valid, validPair) 160 | } 161 | a.set[t.checklist] = cl 162 | // Updating checklist states. 163 | a.updateState() 164 | a.mux.Unlock() 165 | 166 | return nil 167 | } 168 | 169 | var ( 170 | errFingerprintNotFound = errors.New("STUN message fingerprint attribute not found") 171 | errRoleConflict = errors.New("role conflict") 172 | ) 173 | 174 | func (a *Agent) processBindingResponse(t *agentTransaction, p *Pair, m *stun.Message, raddr Addr) error { 175 | integrity := stun.NewShortTermIntegrity(a.remotePassword) 176 | if err := stun.Fingerprint.Check(m); err != nil { 177 | if err == stun.ErrAttributeNotFound { 178 | return errFingerprintNotFound 179 | } 180 | return err 181 | } 182 | if !raddr.Equal(p.Remote.Addr) { 183 | return errNonSymmetricAddr 184 | } 185 | if m.Type == stun.BindingError { 186 | var errCode stun.ErrorCodeAttribute 187 | if err := errCode.GetFrom(m); err != nil { 188 | return err 189 | } 190 | if errCode.Code == stun.CodeRoleConflict { 191 | return errRoleConflict 192 | } 193 | a.log.Debug("got binding error", 194 | zap.String("reason", string(errCode.Reason)), 195 | zap.Int("code", int(errCode.Code)), 196 | ) 197 | return unrecoverableErrorCodeErr{Code: errCode.Code} 198 | } 199 | if err := integrity.Check(m); err != nil { 200 | return err 201 | } 202 | if m.Type != stun.BindingSuccess { 203 | return unexpectedResponseTypeErr{Type: m.Type} 204 | } 205 | var xAddr stun.XORMappedAddress 206 | if err := xAddr.GetFrom(m); err != nil { 207 | return fmt.Errorf("can't get xor mapped address: %v", err) 208 | } 209 | addr := Addr{ 210 | IP: make(net.IP, len(xAddr.IP)), 211 | Port: xAddr.Port, 212 | Proto: p.Local.Addr.Proto, 213 | } 214 | copy(addr.IP, xAddr.IP) 215 | if _, ok := a.localCandidateByAddr(addr); !ok { 216 | if err := a.addPeerReflexive(t, p, addr); err != nil { 217 | return err 218 | } 219 | } 220 | return nil 221 | } 222 | 223 | var errUnsupportedProtocol = errors.New("protocol not supported") 224 | 225 | func (a *Agent) startBinding(p *Pair, m *stun.Message, priority int, t time.Time) error { 226 | if p.Remote.Addr.Proto != candidate.UDP { 227 | return errUnsupportedProtocol 228 | } 229 | c, ok := a.localCandidateByAddr(p.Local.Addr) 230 | if !ok { 231 | return errCandidateNotFound 232 | } 233 | a.mux.Lock() 234 | checklist := a.checklist 235 | a.mux.Unlock() 236 | 237 | at := &agentTransaction{ 238 | id: m.TransactionID, 239 | start: t, 240 | rto: a.rto(), 241 | raw: m.Raw, 242 | checklist: checklist, 243 | priority: priority, 244 | nominate: p.Nominated, 245 | pair: getPairKey(p), 246 | attempt: 1, 247 | maxAttempts: a.maxAttempts, 248 | } 249 | at.setDeadline(t) 250 | 251 | a.tMux.Lock() 252 | a.t[m.TransactionID] = at 253 | a.tMux.Unlock() 254 | 255 | udpAddr := &net.UDPAddr{ 256 | IP: p.Remote.Addr.IP, 257 | Port: p.Remote.Addr.Port, 258 | } 259 | _, err := c.conn.WriteTo(m.Raw, udpAddr) 260 | // TODO: Add write deadline. 261 | // TODO: Check n if needed. 262 | if err != nil { 263 | a.log.Warn("failed to write", 264 | zap.Stringer("to", udpAddr), 265 | zap.Stringer("from", c.candidate.Addr), 266 | zap.Error(err), 267 | ) 268 | 269 | // TODO: If temporary, just perform STUN retries normally. 270 | a.tMux.Lock() 271 | delete(a.t, m.TransactionID) 272 | a.tMux.Unlock() 273 | 274 | a.mux.Lock() 275 | cl := a.set[checklist] 276 | for i := range cl.Triggered { 277 | if samePair(&cl.Triggered[i], p) { 278 | cl.Triggered[i].State = PairFailed 279 | } 280 | } 281 | for i := range cl.Pairs { 282 | if samePair(&cl.Pairs[i], p) { 283 | cl.Pairs[i].State = PairFailed 284 | } 285 | } 286 | a.mux.Unlock() 287 | 288 | return nil 289 | } 290 | a.log.Debug("started", 291 | zap.Stringer("remote", udpAddr), 292 | zap.Stringer("msg", m), 293 | ) 294 | return nil 295 | } 296 | 297 | type unexpectedResponseTypeErr struct{ Type stun.MessageType } 298 | 299 | func (e unexpectedResponseTypeErr) Error() string { 300 | return fmt.Sprintf("peer responded with unexpected STUN message %s", e.Type) 301 | } 302 | 303 | type unrecoverableErrorCodeErr struct{ Code stun.ErrorCode } 304 | 305 | func (e unrecoverableErrorCodeErr) Error() string { 306 | return fmt.Sprintf("peer responded with unrecoverable error code %d", e.Code) 307 | } 308 | -------------------------------------------------------------------------------- /e2e/webrtc-chrome/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "log" 10 | "net" 11 | "net/http" 12 | "os" 13 | "sort" 14 | "time" 15 | 16 | "github.com/chromedp/chromedp" 17 | "github.com/chromedp/chromedp/runner" 18 | "github.com/pkg/errors" 19 | "go.uber.org/zap" 20 | "golang.org/x/net/websocket" 21 | 22 | "gortc.io/ice" 23 | "gortc.io/ice/candidate" 24 | iceSDP "gortc.io/ice/sdp" 25 | "gortc.io/sdp" 26 | ) 27 | 28 | var ( 29 | bin = flag.String("b", "/usr/bin/google-chrome", "path to binary") 30 | headless = flag.Bool("headless", true, "headless mode") 31 | httpAddr = flag.String("addr", "0.0.0.0:8080", "http endpoint to listen") 32 | signalingAddr = flag.String("signaling", "signaling:2255", "signaling server addr") 33 | timeout = flag.Duration("timeout", time.Second*5, "test timeout") 34 | controlling = flag.Bool("controlling", false, "agent is controlling") 35 | controlledAddr = flag.String("controlled", "turn-controlled:8080", "controlled server addr") 36 | browser = flag.Bool("browser", false, "use browser as ICE agent") 37 | ) 38 | 39 | func resolve(a string) *net.TCPAddr { 40 | for i := 0; i < 10; i++ { 41 | addr, err := net.ResolveTCPAddr("tcp", a) 42 | if err == nil { 43 | log.Println("resolved", a, "->", addr) 44 | return addr 45 | } 46 | time.Sleep(time.Millisecond * 100 * time.Duration(i)) 47 | } 48 | panic(fmt.Sprintf("failed to resolve %q", a)) 49 | } 50 | 51 | type dpLogEntry struct { 52 | Method string `json:"method"` 53 | Params struct { 54 | Args []struct { 55 | Type string `json:"type"` 56 | Value string `json:"value"` 57 | } `json:"args"` 58 | } `json:"params"` 59 | } 60 | 61 | type sdpDescription struct { 62 | Type string `json:"type"` 63 | SDP string `json:"sdp"` 64 | } 65 | 66 | type iceDescription struct { 67 | Candidate string `json:"candidate"` 68 | } 69 | 70 | type sdpSignal struct { 71 | SDP sdpDescription `json:"sdp"` 72 | ICE iceDescription `json:"ice"` 73 | Signal string `json:"signal"` 74 | Success bool `json:"success,omitempty"` 75 | } 76 | 77 | type sdpAnswer struct { 78 | Candidates []ice.Candidate 79 | Password string 80 | Username string 81 | Offer *sdp.Message 82 | } 83 | 84 | func newAnswer(o sdpAnswer) (string, error) { 85 | firstCandidate := o.Candidates[0] 86 | origin := sdp.Origin{ 87 | Username: "-", 88 | AddressType: "IP4", 89 | Address: "127.0.0.1", 90 | NetworkType: "IN", 91 | 92 | SessionID: o.Offer.Origin.SessionID, 93 | SessionVersion: o.Offer.Origin.SessionVersion, 94 | } 95 | group := o.Offer.Medias[0].Attribute("mid") 96 | m := &sdp.Message{ 97 | Origin: origin, 98 | Name: "-", 99 | Timing: []sdp.Timing{ 100 | { 101 | Start: time.Time{}, // 0 102 | End: time.Time{}, // 0 103 | }, 104 | }, 105 | } 106 | m.AddAttribute("group", "bundle "+group) 107 | m.AddAttribute("msid-semantic", " WMS") 108 | media := sdp.Media{ 109 | Description: sdp.MediaDescription{ 110 | Type: "application", 111 | Port: firstCandidate.Addr.Port, 112 | Protocol: "DTLS/SCTP", 113 | Formats: []string{ 114 | "5000", 115 | }, 116 | }, 117 | Connection: sdp.ConnectionData{ 118 | NetworkType: "IN", 119 | AddressType: "IP4", 120 | IP: firstCandidate.Addr.IP, 121 | }, 122 | } 123 | for _, c := range o.Candidates { 124 | foundationInt := binary.BigEndian.Uint32(c.Foundation) 125 | sdpCandidate := iceSDP.Candidate{ 126 | ComponentID: c.ComponentID, 127 | Priority: c.Priority, 128 | Foundation: int(foundationInt), 129 | Type: c.Type, 130 | Port: c.Addr.Port, 131 | ConnectionAddress: iceSDP.Address{ 132 | Type: iceSDP.AddressIPv4, 133 | IP: c.Addr.IP, 134 | }, 135 | Generation: 0, 136 | NetworkCost: 999, 137 | } 138 | media.AddAttribute("candidate", sdpCandidate.String()) 139 | } 140 | for _, a := range []struct { 141 | k, v string 142 | }{ 143 | {"ice-ufrag", o.Username}, 144 | {"ice-pwd", o.Password}, 145 | // TODO: Use real fingerprint. 146 | {"fingerprint", "sha-256 2A:C8:67:82:83:42:8E:AD:00:D3:3E:63:49:A8:78:94:6D:CB:1C:56:72:15:7D:BA:BE:45:14:8D:FA:EA:05:79"}, 147 | {"setup", "passive"}, 148 | {"mid", group}, 149 | {"sctpmap", "5000 webrtc-datachannel 1024"}, 150 | } { 151 | media.AddAttribute(a.k, a.v) 152 | } 153 | m.Medias = append(m.Medias, media) 154 | s := m.Append(nil) 155 | buf := s.AppendTo(nil) 156 | return string(buf), nil 157 | } 158 | 159 | func startNative(ctx context.Context) error { 160 | const pwd = "P5Ya0tH+WVL4u6rPbt+uMXlk" 161 | const ufrag = "3BFm" 162 | 163 | if *controlling { 164 | return errors.New("controlling native client is not implemented") 165 | } 166 | signalingURL := fmt.Sprintf("ws://%s/ws", resolve(*signalingAddr)) 167 | ws, err := websocket.Dial(signalingURL, "", "http://127.0.0.1:8080") 168 | if err != nil { 169 | return errors.Wrap(err, "failed to initialize ws") 170 | } 171 | defer func() { 172 | _ = ws.Close() 173 | }() 174 | messages := make(chan *sdp.Message) 175 | gotDescription := make(chan struct{}) 176 | go func() { 177 | for { 178 | buf := make([]byte, 1024) 179 | n, err := ws.Read(buf) 180 | if err != nil { 181 | log.Println("read failed:", err) 182 | break 183 | } else { 184 | sig := new(sdpSignal) 185 | if err := json.Unmarshal(buf[:n], sig); err != nil { 186 | log.Fatalln("failed to unmarshal json:", err) 187 | } 188 | if sig.SDP.SDP != "" { 189 | fmt.Println(string(sig.SDP.SDP)) 190 | var s sdp.Session 191 | s, err := sdp.DecodeSession([]byte(sig.SDP.SDP), s) 192 | if err != nil { 193 | log.Fatalln("failed to decode SDP:", err) 194 | } 195 | d := sdp.NewDecoder(s) 196 | m := new(sdp.Message) 197 | if err = d.Decode(m); err != nil { 198 | log.Println("failed to decode SDP message:", err) 199 | } 200 | e := json.NewEncoder(os.Stderr) 201 | e.SetIndent("", " ") 202 | if err = e.Encode(m); err != nil { 203 | log.Println("failed to encode json") 204 | } 205 | media := m.Medias[0] 206 | fmt.Println("ufrag:", media.Attribute("ice-ufrag"), "pwd:", media.Attribute("ice-pwd")) 207 | messages <- m 208 | } else if sig.ICE.Candidate != "" { 209 | var c iceSDP.Candidate 210 | if err := iceSDP.ParseAttribute([]byte(sig.ICE.Candidate), &c); err != nil { 211 | log.Fatalln("failed to parse ICE candidate:", err) 212 | } 213 | log.Println("parsed ICE candidate:", c.ConnectionAddress, c.ComponentID) 214 | } else if sig.Signal != "" { 215 | switch sig.Signal { 216 | case "gotDescription": 217 | gotDescription <- struct{}{} 218 | } 219 | } else { 220 | log.Printf("got %s", buf[:n]) 221 | } 222 | } 223 | } 224 | }() 225 | log.Println("notifying about initialization") 226 | _, port, err := net.SplitHostPort(*httpAddr) 227 | if err != nil { 228 | return err 229 | } 230 | postURL := fmt.Sprintf("http://127.0.0.1:%s/initialized", port) 231 | resp, err := http.Post(postURL, "text/plain", nil) 232 | if err != nil { 233 | return errors.Wrap(err, "failed to POST /initialized") 234 | } 235 | if resp.StatusCode != http.StatusOK { 236 | return errors.Errorf("bad code %d", resp.StatusCode) 237 | } 238 | logger, err := zap.NewDevelopment() 239 | if err != nil { 240 | return err 241 | } 242 | a, err := ice.NewAgent(ice.WithLogger(logger), ice.WithRole(ice.Controlled), ice.WithIPv4Only) 243 | if err != nil { 244 | return err 245 | } 246 | defer func() { 247 | if err := a.Close(); err != nil { 248 | log.Println("failed to close agent:", err) 249 | } 250 | }() 251 | a.SetLocalCredentials(ufrag, pwd) 252 | if err = a.GatherCandidates(); err != nil { 253 | return errors.Wrap(err, "failed to gather candidates") 254 | } 255 | for { 256 | select { 257 | case <-ctx.Done(): 258 | return ctx.Err() 259 | case <-gotDescription: 260 | log.Println("trying to conclude") 261 | if err = a.Conclude(ctx); err != nil { 262 | return err 263 | } 264 | msg, err := json.Marshal(sdpSignal{ 265 | Success: true, 266 | }) 267 | fmt.Println("sending", string(msg)) 268 | if err != nil { 269 | log.Fatalln(err) 270 | } 271 | if _, err = ws.Write(msg); err != nil { 272 | log.Fatalln("failed to write to ws") 273 | } 274 | return nil 275 | case m := <-messages: 276 | log.Println("got offer:", len(m.Medias), "stream(s)") 277 | media := m.Medias[0] 278 | var candidates []ice.Candidate 279 | for _, rawCandidate := range media.Attributes.Values("candidate") { 280 | var c iceSDP.Candidate 281 | if err := iceSDP.ParseAttribute([]byte(rawCandidate), &c); err != nil { 282 | log.Fatalln("failed to parse ICE candidate:", err) 283 | } 284 | cnd := ice.Candidate{ 285 | Type: candidate.Host, 286 | ComponentID: c.ComponentID, 287 | Addr: ice.Addr{ 288 | IP: c.ConnectionAddress.IP, 289 | Port: c.Port, 290 | }, 291 | Priority: c.Priority, 292 | } 293 | cnd.Foundation = ice.Foundation(&cnd, ice.Addr{}) 294 | candidates = append(candidates, cnd) 295 | log.Println("added candidate", cnd.Addr) 296 | } 297 | if err := a.AddRemoteCandidates(candidates); err != nil { 298 | log.Fatalln("failed to add remote candidates:", err) 299 | } 300 | a.SetRemoteCredentials(media.Attribute("ice-ufrag"), media.Attribute("ice-pwd")) 301 | if err := a.PrepareChecklistSet(); err != nil { 302 | log.Fatalln("failed to prepare sets:", err) 303 | } 304 | log.Println("sending answer") 305 | localCandidates, err := a.LocalCandidates() 306 | if err != nil { 307 | log.Fatalln(err) 308 | } 309 | sort.Sort(ice.Candidates(localCandidates)) 310 | answer, err := newAnswer(sdpAnswer{ 311 | Candidates: localCandidates, 312 | Username: ufrag, 313 | Password: pwd, 314 | Offer: m, 315 | }) 316 | fmt.Println("answer:", answer) 317 | msg, err := json.Marshal(sdpSignal{ 318 | SDP: sdpDescription{ 319 | Type: "answer", 320 | SDP: answer, 321 | }, 322 | }) 323 | fmt.Println("sending", string(msg)) 324 | if err != nil { 325 | log.Fatalln(err) 326 | } 327 | if _, err = ws.Write(msg); err != nil { 328 | log.Fatalln("failed to write to ws") 329 | } 330 | log.Println("checklist init OK, wrote answer") 331 | } 332 | } 333 | return nil 334 | } 335 | 336 | func startBrowser(ctx context.Context) error { 337 | c, err := chromedp.New(ctx, chromedp.WithLog(func(s string, i ...interface{}) { 338 | var entry dpLogEntry 339 | if err := json.Unmarshal([]byte(i[0].(string)), &entry); err != nil { 340 | log.Fatalln(err) 341 | } 342 | if entry.Method == "Runtime.consoleAPICalled" { 343 | for _, a := range entry.Params.Args { 344 | log.Println("agent:", a.Value) 345 | } 346 | } 347 | }), chromedp.WithRunnerOptions( 348 | runner.Path(*bin), runner.DisableGPU, runner.Flag("headless", *headless), 349 | )) 350 | if err != nil { 351 | return errors.Wrap(err, "failed to create chrome") 352 | } 353 | if err := c.Run(ctx, chromedp.Navigate("http://"+*httpAddr)); err != nil { 354 | return errors.Wrap(err, "failed to navigate") 355 | } 356 | return nil 357 | } 358 | 359 | func main() { 360 | flag.Parse() 361 | fmt.Println("bin", *bin, "addr", *httpAddr, "timeout", *timeout) 362 | fs := http.FileServer(http.Dir("static")) 363 | http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { 364 | log.Println("http:", request.Method, request.URL.Path, request.RemoteAddr) 365 | fs.ServeHTTP(writer, request) 366 | }) 367 | gotSuccess := make(chan struct{}) 368 | initialized := make(chan struct{}) 369 | http.HandleFunc("/initialized", func(writer http.ResponseWriter, request *http.Request) { 370 | log.Println("http:", request.Method, request.URL.Path, request.RemoteAddr) 371 | switch request.Method { 372 | case http.MethodPost: 373 | // Should be called by browser after initializing websocket conn. 374 | initialized <- struct{}{} 375 | case http.MethodGet: 376 | // Should be called by controlling agent to wait until controlled init. 377 | <-initialized 378 | } 379 | }) 380 | http.HandleFunc("/success", func(writer http.ResponseWriter, request *http.Request) { 381 | gotSuccess <- struct{}{} 382 | }) 383 | http.HandleFunc("/config", func(writer http.ResponseWriter, request *http.Request) { 384 | log.Println("http:", request.Method, request.URL.Path, request.RemoteAddr) 385 | if *controlling { 386 | // Waiting for controlled agent to start. 387 | log.Println("waiting for controlled agent init") 388 | getAddr := resolve(*controlledAddr) 389 | getURL := fmt.Sprintf("http://%s/initialized", getAddr) 390 | res, getErr := http.Get(getURL) 391 | if getErr != nil { 392 | log.Fatalln("failed to get:", getErr) 393 | } 394 | if res.StatusCode != http.StatusOK { 395 | log.Fatalln("bad status", res.Status) 396 | } 397 | log.Println("controlled agent initialized") 398 | } 399 | encoder := json.NewEncoder(writer) 400 | if encodeErr := encoder.Encode(struct { 401 | Controlling bool `json:"controlling"` 402 | Signaling string `json:"signaling"` 403 | }{ 404 | Controlling: *controlling, 405 | Signaling: fmt.Sprintf("ws://%s/ws", resolve(*signalingAddr)), 406 | }); encodeErr != nil { 407 | log.Fatal(encodeErr) 408 | } 409 | }) 410 | go func() { 411 | if err := http.ListenAndServe(*httpAddr, nil); err != nil { 412 | log.Fatalln("failed to listen:", err) 413 | } 414 | }() 415 | ctx, cancel := context.WithTimeout(context.Background(), *timeout) 416 | defer cancel() 417 | if *browser { 418 | log.Println("running in browser") 419 | if err := startBrowser(ctx); err != nil { 420 | log.Fatalln("failed to run in browser mode:", err) 421 | } 422 | } else { 423 | log.Println("running in native mode") 424 | go func() { 425 | // TODO: Use dataChannels when implemented. 426 | if err := startNative(ctx); err != nil { 427 | log.Fatalln("failed to run native:", err) 428 | } 429 | gotSuccess <- struct{}{} 430 | }() 431 | } 432 | select { 433 | case <-gotSuccess: 434 | log.Println("succeeded") 435 | case <-ctx.Done(): 436 | log.Fatalln(ctx.Err()) 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /sdp/sdp_test.go: -------------------------------------------------------------------------------- 1 | package sdp 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "gortc.io/ice/candidate" 12 | "gortc.io/sdp" 13 | ) 14 | 15 | func TestAttributes_Value(t *testing.T) { 16 | a := Attributes{ 17 | {Key: []byte("key"), Value: []byte("value")}, 18 | } 19 | t.Run("Get key", func(t *testing.T) { 20 | v := a.Value([]byte("key")) 21 | if !bytes.Equal(v, []byte("value")) { 22 | t.Error("attr[key] not equal to value") 23 | } 24 | }) 25 | t.Run("Nil", func(t *testing.T) { 26 | v := a.Value([]byte("1")) 27 | if v != nil { 28 | t.Error("attr[1] should be nil") 29 | } 30 | }) 31 | } 32 | 33 | func TestAttribute_String(t *testing.T) { 34 | for _, tt := range []struct { 35 | in Attribute 36 | out string 37 | }{ 38 | {Attribute{}, ":"}, 39 | {Attribute{Key: []byte("k")}, "k:"}, 40 | {Attribute{Value: []byte("v")}, ":v"}, 41 | {Attribute{Key: []byte("k"), Value: []byte("v")}, "k:v"}, 42 | } { 43 | t.Run(tt.out, func(t *testing.T) { 44 | if tt.out != tt.in.String() { 45 | t.Errorf("%q", tt.in.String()) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestAttributes_Equal(t *testing.T) { 52 | for _, tt := range []struct { 53 | name string 54 | a, b Attributes 55 | equal bool 56 | }{ 57 | { 58 | name: "Blank", 59 | equal: true, 60 | }, 61 | { 62 | name: "Equal", 63 | a: Attributes{ 64 | {Key: []byte{1}, Value: []byte{2}}, 65 | }, 66 | b: Attributes{ 67 | {Key: []byte{1}, Value: []byte{2}}, 68 | }, 69 | equal: true, 70 | }, 71 | { 72 | name: "Length", 73 | a: Attributes{ 74 | {Key: []byte{1}, Value: []byte{2}}, 75 | }, 76 | equal: false, 77 | }, 78 | { 79 | name: "Value", 80 | a: Attributes{ 81 | {Key: []byte{1}, Value: []byte{2}}, 82 | }, 83 | b: Attributes{ 84 | {Key: []byte{1}, Value: []byte{3}}, 85 | }, 86 | equal: false, 87 | }, 88 | { 89 | name: "Key", 90 | a: Attributes{ 91 | {Key: []byte{1}, Value: []byte{3}}, 92 | }, 93 | b: Attributes{ 94 | {Key: []byte{2}, Value: []byte{3}}, 95 | }, 96 | equal: false, 97 | }, 98 | { 99 | name: "Values", 100 | a: Attributes{ 101 | {Key: []byte{1}, Value: []byte{2}}, 102 | {Key: []byte{2}, Value: []byte{5}}, 103 | }, 104 | b: Attributes{ 105 | {Key: []byte{1}, Value: []byte{2}}, 106 | }, 107 | equal: false, 108 | }, 109 | { 110 | name: "ValuesB", 111 | a: Attributes{ 112 | {Key: []byte{2}, Value: []byte{1}}, 113 | }, 114 | b: Attributes{ 115 | {Key: []byte{1}, Value: []byte{2}}, 116 | {Key: []byte{2}, Value: []byte{1}}, 117 | }, 118 | equal: false, 119 | }, 120 | { 121 | name: "ValuesDuplicate", 122 | a: Attributes{ 123 | {Key: []byte{1}, Value: []byte{1}}, 124 | {Key: []byte{1}, Value: []byte{1}}, 125 | }, 126 | b: Attributes{ 127 | {Key: []byte{2}, Value: []byte{1}}, 128 | {Key: []byte{1}, Value: []byte{1}}, 129 | }, 130 | equal: false, 131 | }, 132 | } { 133 | t.Run(tt.name, func(t *testing.T) { 134 | if tt.a.Equal(tt.b) != tt.equal { 135 | t.Error("check failed") 136 | } 137 | }) 138 | } 139 | } 140 | 141 | func TestCandidate_Reset(t *testing.T) { 142 | b := Candidate{ 143 | Foundation: 3862931549, 144 | ComponentID: 1, 145 | Priority: 2113937151, 146 | ConnectionAddress: Address{ 147 | IP: net.ParseIP("192.168.220.128"), 148 | }, 149 | Port: 56032, 150 | Type: candidate.Host, 151 | NetworkCost: 50, 152 | Attributes: Attributes{ 153 | Attribute{ 154 | Key: []byte("alpha"), 155 | Value: []byte("beta"), 156 | }, 157 | }, 158 | } 159 | c := Candidate{ 160 | Foundation: 3862931549, 161 | ComponentID: 1, 162 | Priority: 2113937151, 163 | ConnectionAddress: Address{ 164 | IP: net.ParseIP("192.168.220.128"), 165 | }, 166 | Port: 56032, 167 | Type: candidate.Host, 168 | NetworkCost: 50, 169 | Attributes: Attributes{ 170 | Attribute{ 171 | Key: []byte("alpha"), 172 | Value: []byte("beta"), 173 | }, 174 | }, 175 | } 176 | c.Reset() 177 | if c.Equal(&b) { 178 | t.Fatal("should not equal") 179 | } 180 | } 181 | 182 | func TestCandidate_Equal(t *testing.T) { 183 | for _, tt := range []struct { 184 | name string 185 | a, b Candidate 186 | equal bool 187 | }{ 188 | { 189 | name: "Blank", 190 | a: Candidate{}, 191 | b: Candidate{}, 192 | equal: true, 193 | }, 194 | { 195 | name: "Attributes", 196 | a: Candidate{}, 197 | b: Candidate{Attributes: Attributes{{}}}, 198 | equal: false, 199 | }, 200 | { 201 | name: "Port", 202 | a: Candidate{}, 203 | b: Candidate{Port: 10}, 204 | equal: false, 205 | }, 206 | { 207 | name: "Priority", 208 | a: Candidate{}, 209 | b: Candidate{Priority: 10}, 210 | equal: false, 211 | }, 212 | { 213 | name: "Transport", 214 | a: Candidate{Transport: candidate.UDP}, 215 | b: Candidate{Transport: candidate.ProtocolUnknown}, 216 | equal: false, 217 | }, 218 | { 219 | name: "TransportValue", 220 | a: Candidate{}, 221 | b: Candidate{TransportValue: []byte("v")}, 222 | equal: false, 223 | }, 224 | { 225 | name: "Foundation", 226 | a: Candidate{}, 227 | b: Candidate{Foundation: 1}, 228 | equal: false, 229 | }, 230 | { 231 | name: "ComponentID", 232 | a: Candidate{}, 233 | b: Candidate{ComponentID: 1}, 234 | equal: false, 235 | }, 236 | { 237 | name: "NetworkCost", 238 | a: Candidate{}, 239 | b: Candidate{NetworkCost: 1}, 240 | equal: false, 241 | }, 242 | { 243 | name: "Generation", 244 | a: Candidate{}, 245 | b: Candidate{Generation: 1}, 246 | equal: false, 247 | }, 248 | { 249 | name: "Type", 250 | a: Candidate{}, 251 | b: Candidate{Type: 1}, 252 | equal: false, 253 | }, 254 | } { 255 | t.Run(tt.name, func(t *testing.T) { 256 | if tt.a.Equal(&tt.b) != tt.equal { 257 | t.Error("equality test failed") 258 | } 259 | }) 260 | } 261 | } 262 | 263 | func loadData(tb testing.TB, name string) []byte { 264 | name = filepath.Join("testdata", name) 265 | f, err := os.Open(name) 266 | if err != nil { 267 | tb.Fatal(err) 268 | } 269 | defer func() { 270 | if errClose := f.Close(); errClose != nil { 271 | tb.Fatal(errClose) 272 | } 273 | }() 274 | v, err := ioutil.ReadAll(f) 275 | if err != nil { 276 | tb.Fatal(err) 277 | } 278 | return v 279 | } 280 | 281 | func TestCandidate_String(t *testing.T) { 282 | for _, tc := range []struct { 283 | Name string 284 | In Candidate 285 | Out string 286 | }{ 287 | { 288 | Name: "blank", 289 | Out: "0 0 udp 0 0 typ host generation 0", 290 | }, 291 | { 292 | Name: "host", 293 | Out: "3862931549 0 udp 2113937151 10.1.0.5 2001 typ host generation 0 network-cost 50", 294 | In: Candidate{ 295 | ConnectionAddress: Address{ 296 | Type: AddressIPv4, 297 | IP: net.IPv4(10, 1, 0, 5), 298 | }, 299 | Type: candidate.Host, 300 | Port: 2001, 301 | Foundation: 3862931549, 302 | Priority: 2113937151, 303 | Attributes: Attributes{ 304 | { 305 | Key: []byte("network-cost"), 306 | Value: []byte("50"), 307 | }, 308 | }, 309 | }, 310 | }, 311 | { 312 | Name: "network cost", 313 | Out: "3862931549 0 udp 2113937151 10.1.0.5 2001 typ host generation 0 network-cost 999", 314 | In: Candidate{ 315 | ConnectionAddress: Address{ 316 | Type: AddressIPv4, 317 | IP: net.IPv4(10, 1, 0, 5), 318 | }, 319 | Type: candidate.Host, 320 | Port: 2001, 321 | Foundation: 3862931549, 322 | Priority: 2113937151, 323 | NetworkCost: 999, 324 | }, 325 | }, 326 | } { 327 | t.Run(tc.Name, func(t *testing.T) { 328 | if out := tc.In.String(); out != tc.Out { 329 | t.Errorf("%q (got) != %q (expected)", out, tc.Out) 330 | } 331 | }) 332 | } 333 | } 334 | 335 | func BenchmarkAddress_String(b *testing.B) { 336 | b.ReportAllocs() 337 | c := &Candidate{ 338 | ConnectionAddress: Address{ 339 | Type: AddressIPv4, 340 | IP: net.IPv4(10, 1, 0, 5), 341 | }, 342 | Type: candidate.Host, 343 | Port: 2001, 344 | Foundation: 3862931549, 345 | Priority: 2113937151, 346 | Attributes: Attributes{ 347 | { 348 | Key: []byte("network-cost"), 349 | Value: []byte("50"), 350 | }, 351 | }, 352 | } 353 | for i := 0; i < b.N; i++ { 354 | if c.String() == "" { 355 | b.Fatal("blank string") 356 | } 357 | } 358 | } 359 | 360 | func TestConnectionAddress(t *testing.T) { 361 | data := loadData(t, "candidates_ex1.sdp") 362 | s, err := sdp.DecodeSession(data, nil) 363 | if err != nil { 364 | t.Fatal(err) 365 | } 366 | for _, c := range s { 367 | p := candidateParser{ 368 | c: new(Candidate), 369 | buf: c.Value, 370 | } 371 | if err = p.parse(); err != nil { 372 | t.Fatal(err) 373 | } 374 | } 375 | 376 | // a=candidate:3862931549 1 udp 2113937151 192.168.220.128 56032 377 | // foundation ---┘ | | | | | 378 | // component id --------┘ | | | | 379 | // transport -----------┘ | | | 380 | // priority ------------------┘ | | 381 | // conn. address -------------------------------┘ | 382 | // port ------------------------------------------┘ 383 | } 384 | 385 | func TestParse(t *testing.T) { 386 | data := loadData(t, "candidates_ex1.sdp") 387 | s, err := sdp.DecodeSession(data, nil) 388 | if err != nil { 389 | t.Fatal(err) 390 | } 391 | expected := []Candidate{ 392 | { 393 | Foundation: 3862931549, 394 | ComponentID: 1, 395 | Priority: 2113937151, 396 | ConnectionAddress: Address{ 397 | IP: net.ParseIP("192.168.220.128"), 398 | }, 399 | Port: 56032, 400 | Type: candidate.Host, 401 | NetworkCost: 50, 402 | Attributes: Attributes{ 403 | Attribute{ 404 | Key: []byte("alpha"), 405 | Value: []byte("beta"), 406 | }, 407 | }, 408 | }, 409 | } 410 | tCases := []struct { 411 | input []byte 412 | expected Candidate 413 | }{ 414 | {s[0].Value, expected[0]}, // 0 415 | } 416 | 417 | for i, c := range tCases { 418 | parser := candidateParser{ 419 | buf: c.input, 420 | c: new(Candidate), 421 | } 422 | if err := parser.parse(); err != nil { 423 | t.Errorf("[%d]: unexpected error %s", 424 | i, err, 425 | ) 426 | } 427 | if !c.expected.Equal(parser.c) { 428 | t.Errorf("[%d]: %v != %v (exp)", 429 | i, parser.c, c.expected, 430 | ) 431 | } 432 | } 433 | } 434 | 435 | func BenchmarkParse(b *testing.B) { 436 | data := loadData(b, "candidates_ex1.sdp") 437 | s, err := sdp.DecodeSession(data, nil) 438 | if err != nil { 439 | b.Fatal(err) 440 | } 441 | b.ReportAllocs() 442 | value := s[0].Value 443 | p := candidateParser{ 444 | c: new(Candidate), 445 | } 446 | for i := 0; i < b.N; i++ { 447 | p.buf = value 448 | if err = p.parse(); err != nil { 449 | b.Fatal(err) 450 | } 451 | p.c.Reset() 452 | } 453 | } 454 | 455 | func BenchmarkParseIP(b *testing.B) { 456 | v := []byte("127.0.0.2") 457 | var ( 458 | result = make([]byte, net.IPv4len) 459 | ) 460 | b.ReportAllocs() 461 | for i := 0; i < b.N; i++ { 462 | result = parseIP(result, v) 463 | result = result[:net.IPv4len] 464 | } 465 | } 466 | 467 | func TestParseAttribute(t *testing.T) { 468 | data := loadData(t, "candidates_ex1.sdp") 469 | s, err := sdp.DecodeSession(data, nil) 470 | if err != nil { 471 | t.Fatal(err) 472 | } 473 | expected := []Candidate{ 474 | { 475 | Foundation: 3862931549, 476 | ComponentID: 1, 477 | Priority: 2113937151, 478 | ConnectionAddress: Address{ 479 | IP: net.ParseIP("192.168.220.128"), 480 | }, 481 | Port: 56032, 482 | Type: candidate.Host, 483 | NetworkCost: 50, 484 | Attributes: Attributes{ 485 | Attribute{ 486 | Key: []byte("alpha"), 487 | Value: []byte("beta"), 488 | }, 489 | }, 490 | }, 491 | } 492 | tCases := []struct { 493 | input []byte 494 | expected Candidate 495 | }{ 496 | {s[0].Value, expected[0]}, // 0 497 | } 498 | 499 | for i, tc := range tCases { 500 | c := new(Candidate) 501 | if err := ParseAttribute(tc.input, c); err != nil { 502 | t.Errorf("[%d]: unexpected error %s", 503 | i, err, 504 | ) 505 | } 506 | if !tc.expected.Equal(c) { 507 | t.Errorf("[%d]: %v != %v (exp)", 508 | i, c, tc.expected, 509 | ) 510 | } 511 | } 512 | } 513 | 514 | func TestAddressType_String(t *testing.T) { 515 | for _, tt := range []struct { 516 | in AddressType 517 | out string 518 | }{ 519 | {in: AddressIPv4, out: "IPv4"}, 520 | {in: AddressIPv6, out: "IPv6"}, 521 | {in: AddressFQDN, out: "FQDN"}, 522 | {in: AddressFQDN + 10, out: "Unknown"}, 523 | } { 524 | t.Run(tt.out, func(t *testing.T) { 525 | if tt.in.String() != tt.out { 526 | t.Errorf("%q", tt.in.String()) 527 | } 528 | }) 529 | } 530 | } 531 | 532 | func TestConnectionAddress_Equal(t *testing.T) { 533 | for _, tt := range []struct { 534 | name string 535 | a, b Address 536 | equal bool 537 | }{ 538 | { 539 | name: "Blank", 540 | equal: true, 541 | }, 542 | { 543 | name: "HostNonFQDN", 544 | b: Address{ 545 | Host: []byte{1}, 546 | }, 547 | equal: true, 548 | }, 549 | { 550 | name: "HostFQDN", 551 | a: Address{ 552 | Type: AddressFQDN, 553 | }, 554 | b: Address{ 555 | Type: AddressFQDN, 556 | Host: []byte{1}, 557 | }, 558 | equal: false, 559 | }, 560 | { 561 | name: "IP", 562 | b: Address{ 563 | IP: net.IPv4(1, 0, 0, 1), 564 | }, 565 | equal: false, 566 | }, 567 | { 568 | name: "Type", 569 | b: Address{ 570 | Type: AddressIPv6, 571 | }, 572 | equal: false, 573 | }, 574 | } { 575 | t.Run(tt.name, func(t *testing.T) { 576 | if tt.a.Equal(tt.b) != tt.equal { 577 | t.Error("equality test failed") 578 | } 579 | }) 580 | } 581 | } 582 | 583 | func TestConnectionAddress_String(t *testing.T) { 584 | for _, tt := range []struct { 585 | in Address 586 | out string 587 | }{ 588 | { 589 | in: Address{}, 590 | out: "", 591 | }, 592 | { 593 | in: Address{ 594 | Type: AddressFQDN, 595 | Host: []byte("gortc.io"), 596 | }, 597 | out: "gortc.io", 598 | }, 599 | { 600 | in: Address{ 601 | IP: net.IPv4(127, 0, 0, 1), 602 | }, 603 | out: "127.0.0.1", 604 | }, 605 | } { 606 | t.Run(tt.out, func(t *testing.T) { 607 | if tt.in.String() != tt.out { 608 | t.Errorf("%q", tt.in) 609 | } 610 | }) 611 | } 612 | } 613 | 614 | func TestCandidateType_String(t *testing.T) { 615 | for _, tt := range []struct { 616 | in candidate.Type 617 | out string 618 | }{ 619 | {in: candidate.PeerReflexive, out: "Peer-reflexive"}, 620 | {in: candidate.Relayed + 10, out: "Unknown"}, 621 | } { 622 | t.Run(tt.out, func(t *testing.T) { 623 | if tt.in.String() != tt.out { 624 | t.Errorf("%q", tt.in.String()) 625 | } 626 | }) 627 | } 628 | } 629 | -------------------------------------------------------------------------------- /sdp/sdp.go: -------------------------------------------------------------------------------- 1 | // Package sdp implements Session Description Protocol (SDP) Offer/Answer 2 | // procedures for Interactive Connectivity Establishment (ICE). 3 | // 4 | // Currently this implementation is based on the following Internet Draft: 5 | // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-21 6 | package sdp 7 | 8 | import ( 9 | "bytes" 10 | "fmt" 11 | "net" 12 | "strconv" 13 | "strings" 14 | "unsafe" 15 | 16 | ct "gortc.io/ice/candidate" 17 | ) 18 | 19 | // AddressType is type for Address. 20 | type AddressType byte 21 | 22 | // Possible address types. 23 | const ( 24 | AddressIPv4 AddressType = iota 25 | AddressIPv6 26 | AddressFQDN 27 | ) 28 | 29 | func strOrUnknown(str string) string { 30 | if str == "" { 31 | return "Unknown" 32 | } 33 | return str 34 | } 35 | 36 | var addressTypeToStr = map[AddressType]string{ 37 | AddressIPv4: "IPv4", 38 | AddressIPv6: "IPv6", 39 | AddressFQDN: "FQDN", 40 | } 41 | 42 | func (a AddressType) String() string { 43 | return strOrUnknown(addressTypeToStr[a]) 44 | } 45 | 46 | // Address represents address that can be ipv4/6 or FQDN. 47 | type Address struct { 48 | Host []byte 49 | IP net.IP 50 | Type AddressType 51 | } 52 | 53 | // reset sets all fields to zero values. 54 | func (a *Address) reset() { 55 | a.Host = a.Host[:0] 56 | for i := range a.IP { 57 | a.IP[i] = 0 58 | } 59 | a.Type = AddressIPv4 60 | } 61 | 62 | // Equal returns true if b equals to a. 63 | func (a Address) Equal(b Address) bool { 64 | if a.Type != b.Type { 65 | return false 66 | } 67 | switch a.Type { 68 | case AddressFQDN: 69 | return bytes.Equal(a.Host, b.Host) 70 | default: 71 | return a.IP.Equal(b.IP) 72 | } 73 | } 74 | 75 | func (a Address) str() string { 76 | switch a.Type { 77 | case AddressFQDN: 78 | return string(a.Host) 79 | default: 80 | return a.IP.String() 81 | } 82 | } 83 | 84 | func (a Address) String() string { 85 | return a.str() 86 | } 87 | 88 | const ( 89 | sdpCandidateHost = "host" 90 | sdpCandidateServerReflexive = "srflx" 91 | sdpCandidatePeerReflexive = "prflx" 92 | sdpCandidateRelay = "relay" 93 | ) 94 | 95 | // Candidate is parsed ICE candidate from SDP. 96 | // 97 | // This attribute is used with Interactive Connectivity 98 | // Establishment (ICE), and provides one of many possible candidate 99 | // addresses for communication. These addresses are validated with 100 | // an end-to-end connectivity check using Session Traversal Utilities 101 | // for NAT (STUN)). 102 | type Candidate struct { 103 | ConnectionAddress Address 104 | RelatedAddress Address 105 | TransportValue []byte 106 | Attributes Attributes // other 107 | Port int 108 | Foundation int 109 | ComponentID int 110 | Priority int 111 | RelatedPort int 112 | NetworkCost int // extended 113 | Generation int // extended 114 | Transport ct.Protocol 115 | Type ct.Type 116 | } 117 | 118 | // UnmarshalText implements TextUnmarshaler. 119 | func (c *Candidate) UnmarshalText(text []byte) error { 120 | return ParseAttribute(text, c) 121 | } 122 | 123 | // MarshalText implements TextMarshaler. 124 | func (c *Candidate) MarshalText() (text []byte, err error) { 125 | return []byte(c.String()), nil 126 | } 127 | 128 | func transportToStr(t ct.Protocol) string { 129 | switch t { 130 | case ct.UDP: 131 | return "udp" 132 | default: 133 | return "unknown" 134 | } 135 | } 136 | 137 | func typToStr(t ct.Type) string { 138 | switch t { 139 | case ct.Host: 140 | return sdpCandidateHost 141 | case ct.ServerReflexive: 142 | return sdpCandidateServerReflexive 143 | case ct.PeerReflexive: 144 | return sdpCandidatePeerReflexive 145 | case ct.Relayed: 146 | return sdpCandidateRelay 147 | default: 148 | return "unknown" 149 | } 150 | } 151 | 152 | //nolint:gocritic 153 | func (c Candidate) String() string { 154 | parts := []string{ 155 | strconv.Itoa(c.Foundation), 156 | strconv.Itoa(c.ComponentID), 157 | transportToStr(c.Transport), 158 | strconv.Itoa(c.Priority), 159 | c.ConnectionAddress.String(), 160 | strconv.Itoa(c.Port), 161 | aType, typToStr(c.Type), 162 | aGeneration, strconv.Itoa(c.Generation), 163 | } 164 | if c.NetworkCost > 0 { 165 | parts = append(parts, aNetworkCost, strconv.Itoa(c.NetworkCost)) 166 | } 167 | for _, a := range c.Attributes { 168 | parts = append(parts, byteStr(a.Key), byteStr(a.Value)) 169 | } 170 | return strings.Join(parts, " ") 171 | } 172 | 173 | // Reset sets all fields to zero values. 174 | func (c *Candidate) Reset() { 175 | c.ConnectionAddress.reset() 176 | c.RelatedAddress.reset() 177 | c.RelatedPort = 0 178 | c.NetworkCost = 0 179 | c.Generation = 0 180 | c.Transport = ct.ProtocolUnknown 181 | c.TransportValue = c.TransportValue[:0] 182 | c.Attributes = c.Attributes[:0] 183 | } 184 | 185 | // Equal returns true if b candidate is equal to ct. 186 | func (c *Candidate) Equal(b *Candidate) bool { 187 | if !c.ConnectionAddress.Equal(b.ConnectionAddress) { 188 | return false 189 | } 190 | if c.Port != b.Port { 191 | return false 192 | } 193 | if c.Transport != b.Transport { 194 | return false 195 | } 196 | if !bytes.Equal(c.TransportValue, b.TransportValue) { 197 | return false 198 | } 199 | if c.Foundation != b.Foundation { 200 | return false 201 | } 202 | if c.ComponentID != b.ComponentID { 203 | return false 204 | } 205 | if c.Priority != b.Priority { 206 | return false 207 | } 208 | if c.Type != b.Type { 209 | return false 210 | } 211 | if c.NetworkCost != b.NetworkCost { 212 | return false 213 | } 214 | if c.Generation != b.Generation { 215 | return false 216 | } 217 | if !c.Attributes.Equal(b.Attributes) { 218 | return false 219 | } 220 | return true 221 | } 222 | 223 | // Attribute is key-value pair. 224 | type Attribute struct { 225 | Key []byte 226 | Value []byte 227 | } 228 | 229 | // Attributes is list of attributes. 230 | type Attributes []Attribute 231 | 232 | // Value returns first attribute value with key k or 233 | // nil of none found. 234 | func (a Attributes) Value(k []byte) []byte { 235 | for _, attribute := range a { 236 | if bytes.Equal(attribute.Key, k) { 237 | return attribute.Value 238 | } 239 | } 240 | return nil 241 | } 242 | 243 | // Equal returns true if a equals b. 244 | func (a Attributes) Equal(b Attributes) bool { 245 | if len(a) != len(b) { 246 | return false 247 | } 248 | for _, attr := range a { 249 | v := b.Value(attr.Key) 250 | if !bytes.Equal(v, attr.Value) { 251 | return false 252 | } 253 | } 254 | for _, attr := range b { 255 | v := a.Value(attr.Key) 256 | if !bytes.Equal(v, attr.Value) { 257 | return false 258 | } 259 | } 260 | return true 261 | } 262 | 263 | func byteStr(b []byte) string { 264 | if b == nil { 265 | return "" 266 | } 267 | return string(b) 268 | } 269 | 270 | func (a Attribute) String() string { 271 | return fmt.Sprintf("%v:%v", byteStr(a.Key), byteStr(a.Value)) 272 | } 273 | 274 | // candidateParser should parse []byte into Candidate. 275 | // 276 | // a=candidate:3862931549 1 udp 2113937151 192.168.1.2 56032 typ host generation 0 network-cost 50 277 | // foundation ---┘ | | | | | 278 | // component id --------┘ | | | | 279 | // transport -----------┘ | | | 280 | // priority ------------------┘ | | 281 | // conn. address -------------------------------┘ | 282 | // port -----------------------------------------┘ 283 | type candidateParser struct { 284 | buf []byte 285 | c *Candidate 286 | } 287 | 288 | const sp = ' ' 289 | 290 | const ( 291 | mandatoryElements = 6 292 | ) 293 | 294 | func parseInt(v []byte) (int, error) { 295 | return strconv.Atoi(b2s(v)) 296 | } 297 | 298 | func (p *candidateParser) parseFoundation(v []byte) error { 299 | i, err := parseInt(v) 300 | if err != nil { 301 | return fmt.Errorf("failed to parse foundation: %v", err) 302 | } 303 | p.c.Foundation = i 304 | return nil 305 | } 306 | 307 | func (p *candidateParser) parseComponentID(v []byte) error { 308 | i, err := parseInt(v) 309 | if err != nil { 310 | return fmt.Errorf("failed to parse component ID: %v", err) 311 | } 312 | p.c.ComponentID = i 313 | return nil 314 | } 315 | 316 | func (p *candidateParser) parsePriority(v []byte) error { 317 | i, err := parseInt(v) 318 | if err != nil { 319 | return fmt.Errorf("failed to parse priority: %v", err) 320 | } 321 | p.c.Priority = i 322 | return nil 323 | } 324 | 325 | func (p *candidateParser) parsePort(v []byte) error { 326 | i, err := parseInt(v) 327 | if err != nil { 328 | return fmt.Errorf("failed to parse port: %v", err) 329 | } 330 | p.c.Port = i 331 | return nil 332 | } 333 | 334 | func (p *candidateParser) parseRelatedPort(v []byte) error { 335 | i, err := parseInt(v) 336 | if err != nil { 337 | return fmt.Errorf("failed to parse port: %v", err) 338 | } 339 | p.c.RelatedPort = i 340 | return nil 341 | } 342 | 343 | // b2s converts byte slice to a string without memory allocation. 344 | // 345 | // Note it may break if string and/or slice header will change 346 | // in the future go versions. 347 | func b2s(b []byte) string { 348 | return *(*string)(unsafe.Pointer(&b)) // #nosec 349 | } 350 | 351 | func parseIP(dst net.IP, v []byte) net.IP { 352 | ip := net.ParseIP(b2s(v)) 353 | return append(dst, ip...) 354 | } 355 | 356 | func (candidateParser) parseAddress(v []byte, target *Address) error { 357 | target.IP = parseIP(target.IP, v) 358 | if target.IP == nil { 359 | target.Host = v 360 | target.Type = AddressFQDN 361 | return nil 362 | } 363 | target.Type = AddressIPv6 364 | if target.IP.To4() != nil { 365 | target.Type = AddressIPv4 366 | } 367 | return nil 368 | } 369 | 370 | func (p *candidateParser) parseConnectionAddress(v []byte) error { 371 | return p.parseAddress(v, &p.c.ConnectionAddress) 372 | } 373 | 374 | func (p *candidateParser) parseRelatedAddress(v []byte) error { 375 | return p.parseAddress(v, &p.c.RelatedAddress) 376 | } 377 | 378 | func (p *candidateParser) parseTransport(v []byte) error { 379 | if bytes.Equal(v, []byte("udp")) || bytes.Equal(v, []byte("UDP")) { 380 | p.c.Transport = ct.UDP 381 | } else { 382 | p.c.Transport = ct.ProtocolUnknown 383 | p.c.TransportValue = v 384 | } 385 | return nil 386 | } 387 | 388 | // possible attribute keys. 389 | const ( 390 | aGeneration = "generation" 391 | aNetworkCost = "network-cost" 392 | aType = "typ" 393 | aRelatedAddress = "raddr" 394 | aRelatedPort = "rport" 395 | ) 396 | 397 | func (p *candidateParser) parseAttribute(a Attribute) error { 398 | switch string(a.Key) { 399 | case aGeneration: 400 | return p.parseGeneration(a.Value) 401 | case aNetworkCost: 402 | return p.parseNetworkCost(a.Value) 403 | case aType: 404 | return p.parseType(a.Value) 405 | case aRelatedAddress: 406 | return p.parseRelatedAddress(a.Value) 407 | case aRelatedPort: 408 | return p.parseRelatedPort(a.Value) 409 | default: 410 | p.c.Attributes = append(p.c.Attributes, a) 411 | return nil 412 | } 413 | } 414 | 415 | type parseFn func(v []byte) error 416 | 417 | const minBufLen = 10 418 | 419 | // parse populates internal Candidate from buffer. 420 | // 421 | //nolint:gocognit,funlen // TODO: simplify 422 | func (p *candidateParser) parse() error { 423 | if len(p.buf) < minBufLen { 424 | return fmt.Errorf("buffer too small (%d < %d)", len(p.buf), minBufLen) 425 | } 426 | // special cases for raw value support: 427 | if p.buf[0] == 'a' { 428 | p.buf = bytes.TrimPrefix(p.buf, []byte("a=")) 429 | } 430 | if p.buf[0] == 'c' { 431 | p.buf = bytes.TrimPrefix(p.buf, []byte("candidate:")) 432 | } 433 | // pos is current position 434 | // l is value length 435 | // last is last character offset 436 | // of mandatory elements 437 | var pos, l, last int 438 | fns := [...]parseFn{ 439 | p.parseFoundation, // 0 440 | p.parseComponentID, // 1 441 | p.parseTransport, // 2 442 | p.parsePriority, // 3 443 | p.parseConnectionAddress, // 4 444 | p.parsePort, // 5 445 | } 446 | for i, b := range p.buf { 447 | if pos > mandatoryElements-1 { 448 | // saving offset 449 | last = i 450 | break 451 | } 452 | if b != sp { 453 | // non-space character 454 | l++ 455 | continue 456 | } 457 | // space character reached 458 | if err := fns[pos](p.buf[i-l : i]); err != nil { 459 | return fmt.Errorf("failed to parse char %d, pos %d: %v", 460 | i, pos, err, 461 | ) 462 | } 463 | pos++ // next element 464 | l = 0 // reset length of element 465 | } 466 | if last == 0 { 467 | // no non-mandatory elements 468 | return nil 469 | } 470 | // offsets: 471 | var ( 472 | start int // key start 473 | end int // key end 474 | vStart int // value start 475 | ) 476 | // subslicing to simplify offset calculation 477 | buf := p.buf[last-1:] 478 | // saving every k:v pair ignoring spaces 479 | for i, b := range buf { 480 | if b != sp && i != len(buf)-1 { 481 | // b is non-space or end of buffer 482 | if start == 0 { 483 | // key not started 484 | start = i 485 | continue 486 | } 487 | if vStart == 0 && end != 0 { 488 | // value not started and key ended 489 | vStart = i 490 | } 491 | continue 492 | } 493 | // b is space or end of buf reached 494 | if start == 0 { 495 | // key not started, skipping 496 | continue 497 | } 498 | if end == 0 { 499 | // key ended, saving offset 500 | end = i 501 | continue 502 | } 503 | if vStart == 0 { 504 | // value not started, skipping 505 | continue 506 | } 507 | if i == len(buf)-1 && buf[len(buf)-1] != sp { 508 | // fix for end of buf 509 | i = len(buf) 510 | } 511 | // value ended, saving attribute 512 | a := Attribute{ 513 | Key: buf[start:end], 514 | Value: buf[vStart:i], 515 | } 516 | if err := p.parseAttribute(a); err != nil { 517 | return fmt.Errorf("failed to parse attribute at char %d: %v", 518 | i+last, err, 519 | ) 520 | } 521 | // reset offset 522 | vStart = 0 523 | end = 0 524 | start = 0 525 | } 526 | return nil 527 | } 528 | 529 | func (p *candidateParser) parseNetworkCost(v []byte) error { 530 | i, err := parseInt(v) 531 | if err != nil { 532 | return fmt.Errorf("failed to parse network cost: %v", err) 533 | } 534 | p.c.NetworkCost = i 535 | return nil 536 | } 537 | 538 | func (p *candidateParser) parseGeneration(v []byte) error { 539 | i, err := parseInt(v) 540 | if err != nil { 541 | return fmt.Errorf("failed to parse generation: %v", err) 542 | } 543 | p.c.Generation = i 544 | return nil 545 | } 546 | 547 | func (p *candidateParser) parseType(v []byte) error { 548 | switch string(v) { 549 | case sdpCandidateHost: 550 | p.c.Type = ct.Host 551 | case sdpCandidatePeerReflexive: 552 | p.c.Type = ct.PeerReflexive 553 | case sdpCandidateRelay: 554 | p.c.Type = ct.Relayed 555 | case sdpCandidateServerReflexive: 556 | p.c.Type = ct.ServerReflexive 557 | default: 558 | return fmt.Errorf("unknown candidate %q", v) 559 | } 560 | return nil 561 | } 562 | 563 | // ParseAttribute parses v into ct and returns error if any. 564 | func ParseAttribute(v []byte, c *Candidate) error { 565 | p := candidateParser{ 566 | buf: v, 567 | c: c, 568 | } 569 | err := p.parse() 570 | return err 571 | } 572 | -------------------------------------------------------------------------------- /agent.go: -------------------------------------------------------------------------------- 1 | package ice 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net" 12 | "sync" 13 | "time" 14 | 15 | "go.uber.org/zap" 16 | 17 | "gortc.io/stun" 18 | "gortc.io/turn" 19 | 20 | ct "gortc.io/ice/candidate" 21 | "gortc.io/ice/gather" 22 | ) 23 | 24 | // Role represents ICE agent role, which can be controlling or controlled. 25 | type Role byte 26 | 27 | // UnmarshalText implements TextUnmarshaler. 28 | func (r *Role) UnmarshalText(text []byte) error { 29 | switch string(text) { 30 | case "controlling": 31 | *r = Controlling 32 | case "controlled": 33 | *r = Controlled 34 | default: 35 | return fmt.Errorf("unknown role %q", text) 36 | } 37 | return nil 38 | } 39 | 40 | // MarshalText implements TextMarshaler. 41 | func (r Role) MarshalText() (text []byte, err error) { 42 | return []byte(r.String()), nil 43 | } 44 | 45 | func (r Role) String() string { 46 | switch r { 47 | case Controlling: 48 | return "controlling" 49 | case Controlled: 50 | return "controlled" 51 | default: 52 | return "unknown" 53 | } 54 | } 55 | 56 | // Possible ICE agent roles. 57 | const ( 58 | Controlling Role = iota 59 | Controlled 60 | ) 61 | 62 | // ChecklistSet represents ordered list of checklists. 63 | type ChecklistSet []Checklist 64 | 65 | const noChecklist = -1 66 | 67 | // Server represents ICE server (TURN or STUN) which will be used for 68 | // connectivity establishment. 69 | type Server struct { 70 | URI []string // STUN or TURN URI list 71 | Username string 72 | Credential string 73 | } 74 | 75 | const defaultMaxChecks = 100 76 | const defaultMaxAttempts = 7 77 | 78 | // NewAgent initializes new ICE agent using provided options and returns error 79 | // if any. 80 | func NewAgent(opts ...AgentOption) (*Agent, error) { 81 | a := &Agent{ 82 | gatherer: systemCandidateGatherer{addr: gather.DefaultGatherer}, 83 | maxChecks: defaultMaxChecks, 84 | ta: defaultAgentTa, 85 | maxAttempts: defaultMaxAttempts, 86 | } 87 | for _, o := range opts { 88 | if err := o(a); err != nil { 89 | return nil, err 90 | } 91 | } 92 | if err := a.init(); err != nil { 93 | return nil, err 94 | } 95 | return a, nil 96 | } 97 | 98 | type stunServerOptions struct { 99 | uri stun.URI 100 | username string 101 | password string 102 | } 103 | 104 | type turnServerOptions struct { 105 | uri turn.URI 106 | username string 107 | password string 108 | } 109 | 110 | // Agent implements ICE Agent. 111 | type Agent struct { 112 | set ChecklistSet 113 | checklist int // index in set or -1 114 | foundations [][]byte 115 | tiebreaker uint64 116 | role Role 117 | state State 118 | ipv4Only bool 119 | rand io.Reader 120 | t map[transactionID]*agentTransaction 121 | tMux sync.Mutex 122 | localCandidates [][]*localUDPCandidate 123 | remoteCandidates [][]Candidate 124 | gatherer candidateGatherer 125 | log *zap.Logger 126 | mux sync.Mutex 127 | 128 | localUsername string 129 | localPassword string 130 | remoteUsername string 131 | remotePassword string 132 | 133 | maxChecks int 134 | maxAttempts int 135 | ta time.Duration // section 15.2, Ta 136 | 137 | turn []turnServerOptions 138 | stun []stunServerOptions 139 | } 140 | 141 | // SetLocalCredentials sets local username fragment and password. 142 | func (a *Agent) SetLocalCredentials(username, password string) { 143 | a.localUsername = username 144 | a.localPassword = password 145 | } 146 | 147 | // Username returns local username fragment. 148 | func (a *Agent) Username() string { return a.localUsername } 149 | 150 | // Password returns local password. 151 | func (a *Agent) Password() string { return a.localPassword } 152 | 153 | // SetRemoteCredentials sets ufrag and password for remote candidate. 154 | func (a *Agent) SetRemoteCredentials(username, password string) { 155 | a.remoteUsername = username 156 | a.remotePassword = password 157 | } 158 | 159 | // tick of ta. 160 | func (a *Agent) tick(t time.Time, metChecklists map[int]bool) error { 161 | a.mux.Lock() 162 | if a.checklist == noChecklist { 163 | _, cID := a.nextChecklist() 164 | if cID == noChecklist { 165 | a.mux.Unlock() 166 | return errNoChecklist 167 | } 168 | a.checklist = cID 169 | } 170 | if a.shouldNominate(a.checklist) { 171 | if err := a.startNomination(a.checklist); err != nil { 172 | a.mux.Unlock() 173 | return err 174 | } 175 | } 176 | pair, err := a.pickPair() 177 | if err != nil { 178 | a.log.Debug("pickPair", zap.Error(err)) 179 | } else { 180 | a.log.Debug("pickPair OK") 181 | } 182 | if err == errNoPair || err == errNoChecklist { 183 | metChecklists[a.checklist] = true 184 | _, cID := a.nextChecklist() 185 | if cID == noChecklist || metChecklists[cID] { 186 | a.mux.Unlock() 187 | return errNoChecklist 188 | } 189 | a.checklist = cID 190 | a.mux.Unlock() 191 | return a.tick(t, metChecklists) 192 | } 193 | if err != nil { 194 | a.mux.Unlock() 195 | return err 196 | } 197 | a.mux.Unlock() 198 | return a.startCheck(pair, t) 199 | } 200 | 201 | // Conclude starts connectivity checks and returns when ICE is fully concluded. 202 | func (a *Agent) Conclude(ctx context.Context) error { 203 | // TODO: Start async job. 204 | ticker := time.NewTicker(a.ta) 205 | defer ticker.Stop() 206 | for { 207 | select { 208 | case t := <-ticker.C: 209 | a.collect(t) 210 | if err := a.tick(t, make(map[int]bool)); err != nil { 211 | return err 212 | } 213 | a.mux.Lock() 214 | state := a.state 215 | a.mux.Unlock() 216 | if state == Completed { 217 | a.log.Debug("concluded") 218 | return nil 219 | } 220 | if state == Failed { 221 | return errors.New("failed") 222 | } 223 | case <-ctx.Done(): 224 | return ctx.Err() 225 | } 226 | } 227 | } 228 | 229 | func (a *Agent) localCandidateByAddr(addr Addr) (candidate *localUDPCandidate, ok bool) { 230 | for _, cs := range a.localCandidates { 231 | for i := range cs { 232 | if addr.Equal(cs[i].candidate.Addr) { 233 | return cs[i], true 234 | } 235 | } 236 | } 237 | return nil, false 238 | } 239 | 240 | // Close immediately stops all transactions and frees underlying resources. 241 | func (a *Agent) Close() error { 242 | for _, streamCandidates := range a.localCandidates { 243 | for i := range streamCandidates { 244 | _ = streamCandidates[i].conn.Close() 245 | } 246 | } 247 | return nil 248 | } 249 | 250 | // GatherCandidates gathers local candidates for single data stream. 251 | func (a *Agent) GatherCandidates() error { 252 | return a.GatherCandidatesForStream(defaultStreamID) 253 | } 254 | 255 | var errStreamAlreadyExist = errors.New("data stream with provided id exists") 256 | 257 | const defaultStreamID = 0 258 | 259 | // LocalCandidates returns list of local candidates for first data stream. 260 | func (a *Agent) LocalCandidates() ([]Candidate, error) { 261 | return a.LocalCandidatesForStream(defaultStreamID) 262 | } 263 | 264 | var errNoStreamFound = errors.New("data stream with provided id not found") 265 | 266 | // LocalCandidatesForStream returns list of local candidates for stream. 267 | func (a *Agent) LocalCandidatesForStream(streamID int) ([]Candidate, error) { 268 | if len(a.localCandidates) <= streamID { 269 | return nil, errNoStreamFound 270 | } 271 | var localCandidates []Candidate 272 | for i := range a.localCandidates[streamID] { 273 | localCandidates = append(localCandidates, a.localCandidates[streamID][i].candidate) 274 | } 275 | return localCandidates, nil 276 | } 277 | 278 | // AddRemoteCandidates adds remote candidate list, associating them with first data 279 | // stream. 280 | func (a *Agent) AddRemoteCandidates(c []Candidate) error { 281 | return a.AddRemoteCandidatesForStream(defaultStreamID, c) 282 | } 283 | 284 | // AddRemoteCandidatesForStream adds remote candidate list, associating 285 | // them with data stream with provided id. 286 | func (a *Agent) AddRemoteCandidatesForStream(streamID int, c []Candidate) error { 287 | if len(a.remoteCandidates) > streamID { 288 | return errStreamAlreadyExist 289 | } 290 | a.remoteCandidates = append(a.remoteCandidates, c) 291 | return nil 292 | } 293 | 294 | var errStreamCountMismatch = errors.New("remote and local stream count mismatch") 295 | 296 | // PrepareChecklistSet initializes checklists for each data stream, generating 297 | // candidate pairs for each local and remote candidates. 298 | func (a *Agent) PrepareChecklistSet() error { 299 | if len(a.remoteCandidates) != len(a.localCandidates) { 300 | return errStreamCountMismatch 301 | } 302 | for streamID := 0; streamID < len(a.localCandidates); streamID++ { 303 | var localCandidates []Candidate 304 | for i := range a.localCandidates[streamID] { 305 | localCandidates = append(localCandidates, a.localCandidates[streamID][i].candidate) 306 | } 307 | pairs := NewPairs(localCandidates, a.remoteCandidates[streamID]) 308 | list := Checklist{Pairs: pairs} 309 | list.ComputePriorities(a.role) 310 | list.Sort() 311 | list.Prune() 312 | list.Limit(a.maxChecks) 313 | a.set = append(a.set, list) 314 | } 315 | return a.init() 316 | } 317 | 318 | const minRTO = time.Millisecond * 500 319 | 320 | // rto calculates RTO based on pairs in checklist set and number of connectivity checks. 321 | func (a *Agent) rto() time.Duration { 322 | // See Section 14.3, RTO. 323 | // RTO = MAX (500ms, Ta * N * (Num-Waiting + Num-In-Progress)) 324 | var n, total int 325 | a.mux.Lock() 326 | for _, c := range a.set { 327 | for i := range c.Pairs { 328 | total++ 329 | if c.Pairs[i].State.In(PairWaiting, PairInProgress) { 330 | n++ 331 | } 332 | } 333 | } 334 | a.mux.Unlock() 335 | rto := time.Duration(total*n) * a.ta 336 | if rto < minRTO { 337 | rto = minRTO 338 | } 339 | return rto 340 | } 341 | 342 | const defaultAgentTa = time.Millisecond * 50 343 | 344 | func (a *Agent) updateState() { 345 | var ( 346 | state = Running 347 | allCompleted = true 348 | allFailed = true 349 | ) 350 | for streamID, c := range a.set { 351 | if a.concluded(streamID) { 352 | a.log.Debug("checklist concluded", zap.Int("stream", streamID)) 353 | c.State = ChecklistCompleted 354 | a.set[streamID] = c 355 | } 356 | switch c.State { 357 | case ChecklistFailed: 358 | allCompleted = false 359 | case ChecklistCompleted: 360 | allFailed = false 361 | default: 362 | allFailed = false 363 | allCompleted = false 364 | } 365 | } 366 | if allCompleted { 367 | state = Completed 368 | } else if allFailed { 369 | state = Failed 370 | } 371 | a.state = state 372 | } 373 | 374 | var errCandidateNotFound = errors.New("candidate not found") 375 | 376 | func (a *Agent) addPeerReflexive(t *agentTransaction, p *Pair, addr Addr) error { 377 | // See https://tools.ietf.org/html/rfc8445#section-7.2.5.3.1 378 | pr := Candidate{ 379 | Type: ct.PeerReflexive, 380 | Base: p.Local.Addr, 381 | Addr: addr, 382 | Priority: t.priority, 383 | } 384 | pr.Foundation = Foundation(&pr, Addr{}) 385 | a.mux.Lock() 386 | defer a.mux.Unlock() 387 | c, ok := a.localCandidateByAddr(p.Local.Addr) 388 | if !ok { 389 | return errCandidateNotFound 390 | } 391 | a.localCandidates[c.stream] = append(a.localCandidates[c.stream], &localUDPCandidate{ 392 | conn: c.conn, 393 | candidate: pr, 394 | stream: c.stream, 395 | }) 396 | return nil 397 | } 398 | 399 | func (a *Agent) setPairState(checklist, pair int, state PairState) { 400 | c := a.set[checklist] 401 | p := c.Pairs[pair] 402 | p.State = state 403 | c.Pairs[pair] = p 404 | a.set[checklist] = c 405 | } 406 | 407 | func (a *Agent) setPairStateByKey(checklist int, k pairKey, state PairState) { 408 | c := a.set[checklist] 409 | for i := range c.Pairs { 410 | if k.Equal(&c.Pairs[i]) { 411 | c.Pairs[i].State = state 412 | break 413 | } 414 | } 415 | a.set[checklist] = c 416 | } 417 | 418 | var ( 419 | errNoPair = errors.New("no pair in checklist can be picked") 420 | errNoChecklist = errors.New("no checklist is active") 421 | ) 422 | 423 | func (a *Agent) pickPair() (*Pair, error) { 424 | if a.checklist == noChecklist { 425 | return nil, errNoChecklist 426 | } 427 | // Step 1. Picking from triggered check queue. 428 | if len(a.set[a.checklist].Triggered) > 0 { 429 | // FIFO. Picking top first. 430 | triggered := a.set[a.checklist].Triggered 431 | pair := triggered[len(triggered)-1] 432 | pair.State = PairInProgress 433 | triggered = triggered[:len(triggered)-1] 434 | a.set[a.checklist].Triggered = triggered 435 | return &pair, nil 436 | } 437 | // Step 2. Handling frozen pairs. 438 | pairs := a.set[a.checklist].Pairs 439 | anyWaiting := false 440 | for id := range pairs { 441 | if pairs[id].State == PairWaiting { 442 | anyWaiting = true 443 | break 444 | } 445 | } 446 | if !anyWaiting { 447 | foundations := make(foundationSet) 448 | for _, checklist := range a.set { 449 | for id := range checklist.Pairs { 450 | if checklist.Pairs[id].State.In(PairInProgress, PairWaiting) { 451 | foundations.Add(checklist.Pairs[id].Foundation) 452 | } 453 | } 454 | } 455 | for id := range pairs { 456 | if pairs[id].State != PairFrozen { 457 | continue 458 | } 459 | if foundations.Contains(pairs[id].Foundation) { 460 | continue 461 | } 462 | a.setPairState(a.checklist, id, PairWaiting) 463 | break // to step 3 464 | } 465 | } 466 | // Step 3. Looking for waiting pairs. 467 | for id := range pairs { 468 | if pairs[id].State == PairWaiting { 469 | a.setPairState(a.checklist, id, PairInProgress) 470 | return &pairs[id], nil 471 | } 472 | } 473 | // Step 4. No check could be performed. 474 | return nil, errNoPair 475 | } 476 | 477 | var errNotSTUNMessage = errors.New("packet is not STUN Message") 478 | 479 | func (a *Agent) getPair(streamID int, k pairKey) (*Pair, bool) { 480 | set := a.set[streamID] 481 | for i := range set.Pairs { 482 | if k.Equal(&set.Pairs[i]) { 483 | return &set.Pairs[i], true 484 | } 485 | } 486 | return nil, false 487 | } 488 | 489 | func (a *Agent) processUDP(buf []byte, c *localUDPCandidate, addr *net.UDPAddr) error { 490 | a.log.Debug("got udp packet", 491 | zap.Stringer("local", c.candidate.Addr), 492 | zap.Stringer("from", addr), 493 | ) 494 | if !stun.IsMessage(buf) { 495 | return errNotSTUNMessage 496 | } 497 | m := &stun.Message{Raw: buf} 498 | if err := m.Decode(); err != nil { 499 | return err 500 | } 501 | a.log.Debug("got message", zap.Stringer("m", m)) 502 | raddr := Addr{Port: addr.Port, IP: addr.IP, Proto: ct.UDP} 503 | if m.Type == stun.BindingRequest { 504 | return a.handleBindingRequest(m, c, raddr) 505 | } 506 | 507 | a.tMux.Lock() 508 | t, ok := a.t[m.TransactionID] 509 | a.tMux.Unlock() 510 | 511 | if !ok { 512 | // Transaction is not found. 513 | a.log.Debug("transaction not found") 514 | return nil 515 | } 516 | 517 | a.mux.Lock() 518 | p, _ := a.getPair(t.checklist, t.pair) 519 | a.mux.Unlock() 520 | 521 | switch m.Type { 522 | case stun.BindingSuccess, stun.BindingError: 523 | return a.handleBindingResponse(t, p, m, raddr) 524 | default: 525 | a.log.Debug("unknown message type", zap.Stringer("t", m.Type)) 526 | } 527 | return nil 528 | } 529 | 530 | func (a *Agent) remoteCandidateByAddr(addr Addr) (Candidate, bool) { 531 | for _, s := range a.remoteCandidates { 532 | for i := range s { 533 | if s[i].Addr.Equal(addr) { 534 | return s[i], true 535 | } 536 | } 537 | } 538 | return Candidate{}, false 539 | } 540 | 541 | var errNonSymmetricAddr = errors.New("peer address is not symmetric") 542 | 543 | func samePair(a, b *Pair) bool { 544 | if a.ComponentID != b.ComponentID { 545 | return false 546 | } 547 | if !a.Local.Addr.Equal(b.Local.Addr) { 548 | return false 549 | } 550 | if !a.Remote.Addr.Equal(b.Remote.Addr) { 551 | return false 552 | } 553 | return true 554 | } 555 | 556 | func (a *Agent) concluded(streamID int) bool { 557 | s := a.set[streamID] 558 | if len(s.Valid) == 0 { 559 | return false 560 | } 561 | comps := make(map[int]bool) 562 | for i := range s.Pairs { 563 | comps[s.Pairs[i].ComponentID] = true 564 | } 565 | nominatedComps := make(map[int]bool) 566 | for i := range s.Valid { 567 | if s.Valid[i].Nominated { 568 | continue 569 | } 570 | nominatedComps[s.Valid[i].ComponentID] = true 571 | } 572 | return len(comps) == len(nominatedComps) 573 | } 574 | 575 | func (a *Agent) shouldNominate(streamID int) bool { 576 | s := a.set[streamID] 577 | if len(s.Valid) == 0 { 578 | return false 579 | } 580 | comps := make(map[int]bool) 581 | for i := range s.Pairs { 582 | comps[s.Pairs[i].ComponentID] = true 583 | } 584 | for i := range s.Valid { 585 | if !comps[s.Valid[i].ComponentID] { 586 | return false 587 | } 588 | } 589 | // TODO: Improve stopping criterion. 590 | return true 591 | } 592 | 593 | func (a *Agent) startNomination(streamID int) error { 594 | s := a.set[streamID] 595 | for i := range s.Valid { 596 | if s.Valid[i].Nominated { 597 | continue 598 | } 599 | pair := s.Valid[i] 600 | pair.Nominated = true 601 | s.Triggered = append(s.Triggered, pair) 602 | a.set[streamID] = s 603 | a.log.Debug("starting nomination") 604 | return nil 605 | } 606 | return errNoPair 607 | } 608 | 609 | // startCheck initializes connectivity check for pair. 610 | func (a *Agent) startCheck(p *Pair, t time.Time) error { 611 | a.log.Debug("startCheck", 612 | zap.Stringer("remote", p.Remote.Addr), 613 | zap.Stringer("local", p.Local.Addr), 614 | zap.Int("component", p.ComponentID), 615 | ) 616 | 617 | // Once the agent has picked a candidate pair for which a connectivity 618 | // check is to be performed, the agent starts a check and sends the 619 | // Binding request from the base associated with the local candidate of 620 | // the pair to the remote candidate of the pair, as described in 621 | // Section 7.2.4. 622 | // See RFC 8445 Section 7.2.2. Forming Credentials. 623 | integrity := stun.NewShortTermIntegrity(a.remotePassword) 624 | // The PRIORITY attribute MUST be included in a Binding request and be 625 | // set to the value computed by the algorithm in Section 5.1.2 for the 626 | // local candidate, but with the candidate type preference of peer- 627 | // reflexive candidates. 628 | localPref := p.Local.LocalPreference 629 | priority := Priority(TypePreference(ct.PeerReflexive), localPref, p.Local.ComponentID) 630 | role := AttrControl{Role: a.role, Tiebreaker: a.tiebreaker} 631 | username := stun.NewUsername(a.remoteUsername + ":" + a.localUsername) 632 | attrs := []stun.Setter{ 633 | stun.TransactionID, stun.BindingRequest, 634 | &username, PriorityAttr(priority), &role, 635 | } 636 | if p.Nominated { 637 | attrs = append(attrs, UseCandidate) 638 | } 639 | attrs = append(attrs, &integrity, stun.Fingerprint) 640 | m := stun.MustBuild(attrs...) 641 | return a.startBinding(p, m, priority, t) 642 | } 643 | 644 | func randUint64(r io.Reader) (uint64, error) { 645 | buf := make([]byte, 8) 646 | _, err := io.ReadFull(r, buf) 647 | if err != nil { 648 | return 0, err 649 | } 650 | return binary.LittleEndian.Uint64(buf), nil 651 | } 652 | 653 | func (a *Agent) nextChecklist() (c Checklist, id int) { 654 | if a.checklist == noChecklist { 655 | for id, c = range a.set { 656 | if c.State == ChecklistRunning { 657 | return c, id 658 | } 659 | } 660 | return Checklist{}, noChecklist 661 | } 662 | // Picking checklist 663 | i := a.checklist + 1 664 | for { 665 | if i >= len(a.set) { 666 | i = 0 667 | } 668 | if a.set[i].State == ChecklistRunning { 669 | return a.set[i], i 670 | } 671 | if i == a.checklist { 672 | // Made a circle, nothing found. 673 | return Checklist{}, noChecklist 674 | } 675 | i++ 676 | } 677 | } 678 | 679 | // init sets initial states for checklist sets. 680 | func (a *Agent) init() error { 681 | if a.log == nil { 682 | a.log = zap.NewNop() 683 | } 684 | if a.ta == 0 { 685 | a.ta = defaultAgentTa 686 | } 687 | if a.t == nil { 688 | a.t = make(map[transactionID]*agentTransaction) 689 | } 690 | if a.rand == nil { 691 | a.rand = rand.Reader 692 | } 693 | // Generating random tiebreaker number. 694 | tbValue, err := randUint64(a.rand) 695 | if err != nil { 696 | return err 697 | } 698 | a.tiebreaker = tbValue 699 | a.foundations = a.foundations[:0] 700 | // Gathering all unique foundations. 701 | foundations := make(foundationSet) 702 | for _, c := range a.set { 703 | for i := range c.Pairs { 704 | pair := c.Pairs[i] 705 | if foundations.Contains(pair.Foundation) { 706 | continue 707 | } 708 | foundations.Add(pair.Foundation) 709 | a.foundations = append(a.foundations, pair.Foundation) 710 | } 711 | } 712 | // For each foundation, the agent sets the state of exactly one 713 | // candidate pair to the Waiting state (unfreezing it). The 714 | // candidate pair to unfreeze is chosen by finding the first 715 | // candidate pair (ordered by the lowest component ID and then the 716 | // highest priority if component IDs are equal) in the first 717 | // checklist (according to the usage-defined checklist set order) 718 | // that has that foundation. 719 | for _, f := range a.foundations { 720 | for _, c := range a.set { 721 | for i := range c.Pairs { 722 | if !bytes.Equal(c.Pairs[i].Foundation, f) { 723 | continue 724 | } 725 | c.Pairs[i].State = PairWaiting 726 | break 727 | } 728 | } 729 | } 730 | a.checklist = noChecklist 731 | return nil 732 | } 733 | --------------------------------------------------------------------------------