├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── main.go └── server_test.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.13.x, 1.14.x] 8 | platform: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.platform }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Test 18 | run: go test ./... 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea 3 | stund -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2016-2018, Aleksandr Razumov 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Test](https://github.com/gortc/stund/workflows/Test/badge.svg) 2 | 3 | # stund 4 | 5 | Basic STUN server based on [gortc/stun](https://github.com/gortc/stun). See [gortc/gortcd](https://github.com/gortc/gortcd) 6 | for complete STUN and TURN server. 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gortc/stund 2 | 3 | require ( 4 | github.com/pkg/errors v0.9.1 5 | github.com/sirupsen/logrus v1.7.0 6 | gortc.io/stun v1.23.0 7 | ) 8 | 9 | go 1.13 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 4 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 5 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 6 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 7 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 8 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 9 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 10 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= 14 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 15 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 16 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 17 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 18 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 19 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 21 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 22 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= 23 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 24 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 25 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 26 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 27 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 28 | gortc.io/stun v1.22.2 h1:pDsirGr1eAX0qU4UsnI/DGtyOy38cTZgcX5Iamn/kSk= 29 | gortc.io/stun v1.22.2/go.mod h1:XD5lpONVyjvV3BgOyJFNo0iv6R2oZB4L+weMqxts+zg= 30 | gortc.io/stun v1.23.0 h1:CpRQFjakCZMwVKTwInKbcCzlBklj62LGzD3NPdFyGrE= 31 | gortc.io/stun v1.23.0/go.mod h1:XD5lpONVyjvV3BgOyJFNo0iv6R2oZB4L+weMqxts+zg= 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net" 8 | "net/http" 9 | _ "net/http/pprof" 10 | "strings" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/sirupsen/logrus" 14 | 15 | "gortc.io/stun" 16 | ) 17 | 18 | var ( 19 | network = flag.String("net", "udp", "network to listen") 20 | address = flag.String("addr", "0.0.0.0:3478", "address to listen") 21 | profile = flag.Bool("profile", false, "profile") 22 | ) 23 | 24 | // Server is RFC 5389 basic server implementation. 25 | // 26 | // Current implementation is UDP only and not utilizes FINGERPRINT mechanism, 27 | // nor ALTERNATE-SERVER, nor credentials mechanisms. It does not support 28 | // backwards compatibility with RFC 3489. 29 | type Server struct { 30 | Addr string 31 | LogAllErrors bool 32 | log Logger 33 | } 34 | 35 | // Logger is used for logging formatted messages. 36 | type Logger interface { 37 | // Printf must have the same semantics as log.Printf. 38 | Printf(format string, args ...interface{}) 39 | } 40 | 41 | var ( 42 | defaultLogger = logrus.New() 43 | software = stun.NewSoftware("gortc/stund") 44 | errNotSTUNMessage = errors.New("not stun message") 45 | ) 46 | 47 | func basicProcess(addr net.Addr, b []byte, req, res *stun.Message) error { 48 | if !stun.IsMessage(b) { 49 | return errNotSTUNMessage 50 | } 51 | if _, err := req.Write(b); err != nil { 52 | return errors.Wrap(err, "failed to read message") 53 | } 54 | var ( 55 | ip net.IP 56 | port int 57 | ) 58 | switch a := addr.(type) { 59 | case *net.UDPAddr: 60 | ip = a.IP 61 | port = a.Port 62 | default: 63 | panic(fmt.Sprintf("unknown addr: %v", addr)) 64 | } 65 | return res.Build(req, 66 | stun.BindingSuccess, 67 | software, 68 | &stun.XORMappedAddress{ 69 | IP: ip, 70 | Port: port, 71 | }, 72 | stun.Fingerprint, 73 | ) 74 | } 75 | 76 | func (s *Server) serveConn(c net.PacketConn, res, req *stun.Message) error { 77 | if c == nil { 78 | return nil 79 | } 80 | buf := make([]byte, 1024) 81 | n, addr, err := c.ReadFrom(buf) 82 | if err != nil { 83 | s.log.Printf("ReadFrom: %v", err) 84 | return nil 85 | } 86 | // s.log().Printf("read %d bytes from %s", n, addr) 87 | if _, err = req.Write(buf[:n]); err != nil { 88 | s.log.Printf("Write: %v", err) 89 | return err 90 | } 91 | if err = basicProcess(addr, buf[:n], req, res); err != nil { 92 | if err == errNotSTUNMessage { 93 | return nil 94 | } 95 | s.log.Printf("basicProcess: %v", err) 96 | return nil 97 | } 98 | _, err = c.WriteTo(res.Raw, addr) 99 | if err != nil { 100 | s.log.Printf("WriteTo: %v", err) 101 | } 102 | return err 103 | } 104 | 105 | // Serve reads packets from connections and responds to BINDING requests. 106 | func (s *Server) Serve(c net.PacketConn) error { 107 | var ( 108 | res = new(stun.Message) 109 | req = new(stun.Message) 110 | ) 111 | for { 112 | if err := s.serveConn(c, res, req); err != nil { 113 | s.log.Printf("serve: %v", err) 114 | return err 115 | } 116 | res.Reset() 117 | req.Reset() 118 | } 119 | } 120 | 121 | // ListenUDPAndServe listens on laddr and process incoming packets. 122 | func ListenUDPAndServe(serverNet, laddr string) error { 123 | c, err := net.ListenPacket(serverNet, laddr) 124 | if err != nil { 125 | return err 126 | } 127 | s := &Server{ 128 | log: defaultLogger, 129 | } 130 | return s.Serve(c) 131 | } 132 | 133 | func normalize(address string) string { 134 | if len(address) == 0 { 135 | address = "0.0.0.0" 136 | } 137 | if !strings.Contains(address, ":") { 138 | address = fmt.Sprintf("%s:%d", address, stun.DefaultPort) 139 | } 140 | return address 141 | } 142 | 143 | func main() { 144 | flag.Parse() 145 | if *profile { 146 | go func() { 147 | log.Println(http.ListenAndServe("localhost:6060", nil)) 148 | }() 149 | } 150 | switch *network { 151 | case "udp": 152 | normalized := normalize(*address) 153 | fmt.Println("gortc/stund listening on", normalized, "via", *network) 154 | log.Fatal(ListenUDPAndServe(*network, normalized)) 155 | default: 156 | log.Fatalln("unsupported network:", *network) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "gortc.io/stun" 8 | ) 9 | 10 | func BenchmarkBasicProcess(b *testing.B) { 11 | m := stun.New() 12 | res := stun.New() 13 | req := stun.New() 14 | b.ReportAllocs() 15 | addr, err := net.ResolveUDPAddr("udp", "213.11.231.1:12341") 16 | if err != nil { 17 | b.Fatal(err) 18 | } 19 | m.TransactionID = stun.NewTransactionID() 20 | stun.NewSoftware("some software").AddTo(m) 21 | m.WriteHeader() 22 | b.SetBytes(int64(len(m.Raw))) 23 | for i := 0; i < b.N; i++ { 24 | res.Reset() 25 | req.Reset() 26 | if err := basicProcess(addr, m.Raw, req, res); err != nil { 27 | b.Fatal(err) 28 | } 29 | } 30 | } 31 | --------------------------------------------------------------------------------