Add a standalone server for STUN that can be hosted independently of the derper, and factor that back into the derper. Fixes #8434 Closes #8435 Closes #10745 Signed-off-by: James Tucker <james@tailscale.com>main
parent
569b91417f
commit
953fa80c6f
@ -0,0 +1,190 @@ |
||||
tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depaware) |
||||
|
||||
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus |
||||
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus |
||||
github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil |
||||
github.com/google/uuid from tailscale.com/tsweb |
||||
github.com/matttproud/golang_protobuf_extensions/pbutil from github.com/prometheus/common/expfmt |
||||
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz |
||||
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus |
||||
github.com/prometheus/client_model/go from github.com/prometheus/client_golang/prometheus+ |
||||
github.com/prometheus/common/expfmt from github.com/prometheus/client_golang/prometheus+ |
||||
github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg from github.com/prometheus/common/expfmt |
||||
github.com/prometheus/common/model from github.com/prometheus/client_golang/prometheus+ |
||||
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus |
||||
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs |
||||
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs |
||||
💣 go4.org/mem from tailscale.com/metrics+ |
||||
go4.org/netipx from tailscale.com/net/tsaddr |
||||
google.golang.org/protobuf/encoding/prototext from github.com/golang/protobuf/proto+ |
||||
google.golang.org/protobuf/encoding/protowire from github.com/golang/protobuf/proto+ |
||||
google.golang.org/protobuf/internal/descfmt from google.golang.org/protobuf/internal/filedesc |
||||
google.golang.org/protobuf/internal/descopts from google.golang.org/protobuf/internal/filedesc+ |
||||
google.golang.org/protobuf/internal/detrand from google.golang.org/protobuf/internal/descfmt+ |
||||
google.golang.org/protobuf/internal/encoding/defval from google.golang.org/protobuf/internal/encoding/tag+ |
||||
google.golang.org/protobuf/internal/encoding/messageset from google.golang.org/protobuf/encoding/prototext+ |
||||
google.golang.org/protobuf/internal/encoding/tag from google.golang.org/protobuf/internal/impl |
||||
google.golang.org/protobuf/internal/encoding/text from google.golang.org/protobuf/encoding/prototext+ |
||||
google.golang.org/protobuf/internal/errors from google.golang.org/protobuf/encoding/prototext+ |
||||
google.golang.org/protobuf/internal/filedesc from google.golang.org/protobuf/internal/encoding/tag+ |
||||
google.golang.org/protobuf/internal/filetype from google.golang.org/protobuf/runtime/protoimpl |
||||
google.golang.org/protobuf/internal/flags from google.golang.org/protobuf/encoding/prototext+ |
||||
google.golang.org/protobuf/internal/genid from google.golang.org/protobuf/encoding/prototext+ |
||||
💣 google.golang.org/protobuf/internal/impl from google.golang.org/protobuf/internal/filetype+ |
||||
google.golang.org/protobuf/internal/order from google.golang.org/protobuf/encoding/prototext+ |
||||
google.golang.org/protobuf/internal/pragma from google.golang.org/protobuf/encoding/prototext+ |
||||
google.golang.org/protobuf/internal/set from google.golang.org/protobuf/encoding/prototext |
||||
💣 google.golang.org/protobuf/internal/strs from google.golang.org/protobuf/encoding/prototext+ |
||||
google.golang.org/protobuf/internal/version from google.golang.org/protobuf/runtime/protoimpl |
||||
google.golang.org/protobuf/proto from github.com/golang/protobuf/proto+ |
||||
google.golang.org/protobuf/reflect/protodesc from github.com/golang/protobuf/proto |
||||
💣 google.golang.org/protobuf/reflect/protoreflect from github.com/golang/protobuf/proto+ |
||||
google.golang.org/protobuf/reflect/protoregistry from github.com/golang/protobuf/proto+ |
||||
google.golang.org/protobuf/runtime/protoiface from github.com/golang/protobuf/proto+ |
||||
google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+ |
||||
google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc |
||||
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+ |
||||
tailscale.com from tailscale.com/version |
||||
tailscale.com/envknob from tailscale.com/tsweb+ |
||||
tailscale.com/metrics from tailscale.com/net/stunserver+ |
||||
tailscale.com/net/netaddr from tailscale.com/net/tsaddr |
||||
tailscale.com/net/stun from tailscale.com/net/stunserver |
||||
tailscale.com/net/stunserver from tailscale.com/cmd/stund |
||||
tailscale.com/net/tsaddr from tailscale.com/tsweb |
||||
tailscale.com/tailcfg from tailscale.com/version |
||||
tailscale.com/tsweb from tailscale.com/cmd/stund |
||||
tailscale.com/tsweb/promvarz from tailscale.com/tsweb |
||||
tailscale.com/tsweb/varz from tailscale.com/tsweb+ |
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg |
||||
tailscale.com/types/ipproto from tailscale.com/tailcfg |
||||
tailscale.com/types/key from tailscale.com/tailcfg |
||||
tailscale.com/types/lazy from tailscale.com/version+ |
||||
tailscale.com/types/logger from tailscale.com/tsweb |
||||
tailscale.com/types/opt from tailscale.com/envknob+ |
||||
tailscale.com/types/ptr from tailscale.com/tailcfg |
||||
tailscale.com/types/structs from tailscale.com/tailcfg+ |
||||
tailscale.com/types/tkatype from tailscale.com/tailcfg+ |
||||
tailscale.com/types/views from tailscale.com/net/tsaddr+ |
||||
tailscale.com/util/cmpx from tailscale.com/tailcfg+ |
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics |
||||
tailscale.com/util/dnsname from tailscale.com/tailcfg |
||||
tailscale.com/util/lineread from tailscale.com/version/distro |
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto |
||||
tailscale.com/util/slicesx from tailscale.com/tailcfg |
||||
tailscale.com/util/vizerror from tailscale.com/tailcfg+ |
||||
tailscale.com/version from tailscale.com/envknob+ |
||||
tailscale.com/version/distro from tailscale.com/envknob |
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box |
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305 |
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls |
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+ |
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+ |
||||
golang.org/x/crypto/curve25519 from golang.org/x/crypto/nacl/box+ |
||||
golang.org/x/crypto/hkdf from crypto/tls |
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key |
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box |
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+ |
||||
golang.org/x/net/dns/dnsmessage from net |
||||
golang.org/x/net/http/httpguts from net/http |
||||
golang.org/x/net/http/httpproxy from net/http |
||||
golang.org/x/net/http2/hpack from net/http |
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+ |
||||
D golang.org/x/net/route from net |
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+ |
||||
LD golang.org/x/sys/unix from github.com/prometheus/procfs+ |
||||
W golang.org/x/sys/windows from github.com/prometheus/client_golang/prometheus |
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna |
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+ |
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+ |
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna |
||||
bufio from compress/flate+ |
||||
bytes from bufio+ |
||||
cmp from slices |
||||
compress/flate from compress/gzip |
||||
compress/gzip from github.com/golang/protobuf/proto+ |
||||
container/list from crypto/tls+ |
||||
context from crypto/tls+ |
||||
crypto from crypto/ecdh+ |
||||
crypto/aes from crypto/ecdsa+ |
||||
crypto/cipher from crypto/aes+ |
||||
crypto/des from crypto/tls+ |
||||
crypto/dsa from crypto/x509 |
||||
crypto/ecdh from crypto/ecdsa+ |
||||
crypto/ecdsa from crypto/tls+ |
||||
crypto/ed25519 from crypto/tls+ |
||||
crypto/elliptic from crypto/ecdsa+ |
||||
crypto/hmac from crypto/tls+ |
||||
crypto/md5 from crypto/tls+ |
||||
crypto/rand from crypto/ed25519+ |
||||
crypto/rc4 from crypto/tls |
||||
crypto/rsa from crypto/tls+ |
||||
crypto/sha1 from crypto/tls+ |
||||
crypto/sha256 from crypto/tls+ |
||||
crypto/sha512 from crypto/ecdsa+ |
||||
crypto/subtle from crypto/aes+ |
||||
crypto/tls from net/http+ |
||||
crypto/x509 from crypto/tls |
||||
crypto/x509/pkix from crypto/x509 |
||||
database/sql/driver from github.com/google/uuid |
||||
embed from crypto/internal/nistec+ |
||||
encoding from encoding/json+ |
||||
encoding/asn1 from crypto/x509+ |
||||
encoding/base64 from encoding/json+ |
||||
encoding/binary from compress/gzip+ |
||||
encoding/hex from crypto/x509+ |
||||
encoding/json from expvar+ |
||||
encoding/pem from crypto/tls+ |
||||
errors from bufio+ |
||||
expvar from github.com/prometheus/client_golang/prometheus+ |
||||
flag from tailscale.com/cmd/stund |
||||
fmt from compress/flate+ |
||||
go/token from google.golang.org/protobuf/internal/strs |
||||
hash from crypto+ |
||||
hash/crc32 from compress/gzip+ |
||||
hash/fnv from google.golang.org/protobuf/internal/detrand |
||||
hash/maphash from go4.org/mem |
||||
html from net/http/pprof+ |
||||
io from bufio+ |
||||
io/fs from crypto/x509+ |
||||
io/ioutil from github.com/golang/protobuf/proto+ |
||||
log from expvar+ |
||||
log/internal from log |
||||
maps from tailscale.com/tailcfg+ |
||||
math from compress/flate+ |
||||
math/big from crypto/dsa+ |
||||
math/bits from compress/flate+ |
||||
math/rand from math/big+ |
||||
mime from github.com/prometheus/common/expfmt+ |
||||
mime/multipart from net/http |
||||
mime/quotedprintable from mime/multipart |
||||
net from crypto/tls+ |
||||
net/http from expvar+ |
||||
net/http/httptrace from net/http |
||||
net/http/internal from net/http |
||||
net/http/pprof from tailscale.com/tsweb+ |
||||
net/netip from go4.org/netipx+ |
||||
net/textproto from golang.org/x/net/http/httpguts+ |
||||
net/url from crypto/x509+ |
||||
os from crypto/rand+ |
||||
os/signal from tailscale.com/cmd/stund |
||||
path from github.com/prometheus/client_golang/prometheus/internal+ |
||||
path/filepath from crypto/x509+ |
||||
reflect from crypto/x509+ |
||||
regexp from github.com/prometheus/client_golang/prometheus/internal+ |
||||
regexp/syntax from regexp |
||||
runtime/debug from github.com/prometheus/client_golang/prometheus+ |
||||
runtime/metrics from github.com/prometheus/client_golang/prometheus+ |
||||
runtime/pprof from net/http/pprof |
||||
runtime/trace from net/http/pprof |
||||
slices from tailscale.com/metrics+ |
||||
sort from compress/flate+ |
||||
strconv from compress/flate+ |
||||
strings from bufio+ |
||||
sync from compress/flate+ |
||||
sync/atomic from context+ |
||||
syscall from crypto/rand+ |
||||
text/tabwriter from runtime/pprof |
||||
time from compress/gzip+ |
||||
unicode from bytes+ |
||||
unicode/utf16 from crypto/x509+ |
||||
unicode/utf8 from bufio+ |
||||
@ -0,0 +1,48 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The stund binary is a standalone STUN server.
|
||||
package main |
||||
|
||||
import ( |
||||
"context" |
||||
"flag" |
||||
"io" |
||||
"log" |
||||
"net/http" |
||||
"os/signal" |
||||
"syscall" |
||||
|
||||
"tailscale.com/net/stunserver" |
||||
"tailscale.com/tsweb" |
||||
) |
||||
|
||||
var ( |
||||
stunAddr = flag.String("stun", ":3478", "UDP address on which to start the STUN server") |
||||
httpAddr = flag.String("http", ":3479", "address on which to start the debug http server") |
||||
) |
||||
|
||||
func main() { |
||||
flag.Parse() |
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) |
||||
defer cancel() |
||||
|
||||
log.Printf("HTTP server listening on %s", *httpAddr) |
||||
go http.ListenAndServe(*httpAddr, mux()) |
||||
|
||||
s := stunserver.New(ctx) |
||||
if err := s.ListenAndServe(*stunAddr); err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
} |
||||
|
||||
func mux() *http.ServeMux { |
||||
mux := http.NewServeMux() |
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
||||
io.WriteString(w, "<h1>stund</h1><a href=/debug>/debug</a>") |
||||
}) |
||||
debug := tsweb.Debugger(mux) |
||||
debug.KV("stun_addr", *stunAddr) |
||||
return mux |
||||
} |
||||
@ -0,0 +1,126 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package stunserver implements a STUN server. The package publishes a number of stats
|
||||
// to expvar under the top level label "stun". Logs are sent to the standard log package.
|
||||
package stunserver |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"expvar" |
||||
"io" |
||||
"log" |
||||
"net" |
||||
"net/netip" |
||||
"time" |
||||
|
||||
"tailscale.com/metrics" |
||||
"tailscale.com/net/stun" |
||||
) |
||||
|
||||
var ( |
||||
stats = new(metrics.Set) |
||||
stunDisposition = &metrics.LabelMap{Label: "disposition"} |
||||
stunAddrFamily = &metrics.LabelMap{Label: "family"} |
||||
stunReadError = stunDisposition.Get("read_error") |
||||
stunNotSTUN = stunDisposition.Get("not_stun") |
||||
stunWriteError = stunDisposition.Get("write_error") |
||||
stunSuccess = stunDisposition.Get("success") |
||||
|
||||
stunIPv4 = stunAddrFamily.Get("ipv4") |
||||
stunIPv6 = stunAddrFamily.Get("ipv6") |
||||
) |
||||
|
||||
func init() { |
||||
stats.Set("counter_requests", stunDisposition) |
||||
stats.Set("counter_addrfamily", stunAddrFamily) |
||||
expvar.Publish("stun", stats) |
||||
} |
||||
|
||||
type STUNServer struct { |
||||
ctx context.Context // ctx signals service shutdown
|
||||
pc *net.UDPConn // pc is the UDP listener
|
||||
} |
||||
|
||||
// New creates a new STUN server. The server is shutdown when ctx is done.
|
||||
func New(ctx context.Context) *STUNServer { |
||||
return &STUNServer{ctx: ctx} |
||||
} |
||||
|
||||
// Listen binds the listen socket for the server at listenAddr.
|
||||
func (s *STUNServer) Listen(listenAddr string) error { |
||||
uaddr, err := net.ResolveUDPAddr("udp", listenAddr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
s.pc, err = net.ListenUDP("udp", uaddr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
log.Printf("STUN server listening on %v", s.LocalAddr()) |
||||
// close the listener on shutdown in order to break out of the read loop
|
||||
go func() { |
||||
<-s.ctx.Done() |
||||
s.pc.Close() |
||||
}() |
||||
return nil |
||||
} |
||||
|
||||
// Serve starts serving responses to STUN requests. Listen must be called before Serve.
|
||||
func (s *STUNServer) Serve() error { |
||||
var buf [64 << 10]byte |
||||
var ( |
||||
n int |
||||
ua *net.UDPAddr |
||||
err error |
||||
) |
||||
for { |
||||
n, ua, err = s.pc.ReadFromUDP(buf[:]) |
||||
if err != nil { |
||||
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { |
||||
return nil |
||||
} |
||||
log.Printf("STUN ReadFrom: %v", err) |
||||
time.Sleep(time.Second) |
||||
stunReadError.Add(1) |
||||
continue |
||||
} |
||||
pkt := buf[:n] |
||||
if !stun.Is(pkt) { |
||||
stunNotSTUN.Add(1) |
||||
continue |
||||
} |
||||
txid, err := stun.ParseBindingRequest(pkt) |
||||
if err != nil { |
||||
stunNotSTUN.Add(1) |
||||
continue |
||||
} |
||||
if ua.IP.To4() != nil { |
||||
stunIPv4.Add(1) |
||||
} else { |
||||
stunIPv6.Add(1) |
||||
} |
||||
addr, _ := netip.AddrFromSlice(ua.IP) |
||||
res := stun.Response(txid, netip.AddrPortFrom(addr, uint16(ua.Port))) |
||||
_, err = s.pc.WriteTo(res, ua) |
||||
if err != nil { |
||||
stunWriteError.Add(1) |
||||
} else { |
||||
stunSuccess.Add(1) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// ListenAndServe starts the STUN server on listenAddr.
|
||||
func (s *STUNServer) ListenAndServe(listenAddr string) error { |
||||
if err := s.Listen(listenAddr); err != nil { |
||||
return err |
||||
} |
||||
return s.Serve() |
||||
} |
||||
|
||||
// LocalAddr returns the local address of the STUN server. It must not be called before ListenAndServe.
|
||||
func (s *STUNServer) LocalAddr() net.Addr { |
||||
return s.pc.LocalAddr() |
||||
} |
||||
@ -0,0 +1,88 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package stunserver |
||||
|
||||
import ( |
||||
"context" |
||||
"net" |
||||
"sync" |
||||
"testing" |
||||
"time" |
||||
|
||||
"tailscale.com/net/stun" |
||||
"tailscale.com/util/must" |
||||
) |
||||
|
||||
func TestSTUNServer(t *testing.T) { |
||||
ctx, cancel := context.WithCancel(context.Background()) |
||||
defer cancel() |
||||
s := New(ctx) |
||||
must.Do(s.Listen("localhost:0")) |
||||
var w sync.WaitGroup |
||||
w.Add(1) |
||||
var serveErr error |
||||
go func() { |
||||
defer w.Done() |
||||
serveErr = s.Serve() |
||||
}() |
||||
|
||||
c := must.Get(net.DialUDP("udp", nil, s.LocalAddr().(*net.UDPAddr))) |
||||
defer c.Close() |
||||
c.SetDeadline(time.Now().Add(5 * time.Second)) |
||||
txid := stun.NewTxID() |
||||
_, err := c.Write(stun.Request(txid)) |
||||
if err != nil { |
||||
t.Fatalf("failed to write STUN request: %v", err) |
||||
} |
||||
var buf [64 << 10]byte |
||||
n, err := c.Read(buf[:]) |
||||
if err != nil { |
||||
t.Fatalf("failed to read STUN response: %v", err) |
||||
} |
||||
if !stun.Is(buf[:n]) { |
||||
t.Fatalf("response is not STUN") |
||||
} |
||||
tid, _, err := stun.ParseResponse(buf[:n]) |
||||
if err != nil { |
||||
t.Fatalf("failed to parse STUN response: %v", err) |
||||
} |
||||
if tid != txid { |
||||
t.Fatalf("STUN response has wrong transaction ID; got %d, want %d", tid, txid) |
||||
} |
||||
|
||||
cancel() |
||||
w.Wait() |
||||
if serveErr != nil { |
||||
t.Fatalf("failed to listen and serve: %v", serveErr) |
||||
} |
||||
} |
||||
|
||||
func BenchmarkServerSTUN(b *testing.B) { |
||||
b.ReportAllocs() |
||||
ctx, cancel := context.WithCancel(context.Background()) |
||||
defer cancel() |
||||
|
||||
s := New(ctx) |
||||
s.Listen("localhost:0") |
||||
go s.Serve() |
||||
addr := s.LocalAddr().(*net.UDPAddr) |
||||
|
||||
var resBuf [1500]byte |
||||
cc, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")}) |
||||
if err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
|
||||
tx := stun.NewTxID() |
||||
req := stun.Request(tx) |
||||
for i := 0; i < b.N; i++ { |
||||
if _, err := cc.WriteToUDP(req, addr); err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
_, _, err := cc.ReadFromUDP(resBuf[:]) |
||||
if err != nil { |
||||
b.Fatal(err) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue