From f80e9af1568da7f718c254e0e7f67e1faaaf27dd Mon Sep 17 00:00:00 2001 From: "Lawrence, Rendall" Date: Wed, 30 Nov 2022 19:10:23 +0300 Subject: [PATCH] (untested) add salt to UDP connection ID --- frontend/udp/connection_id.go | 98 ++++++++++++++++++++---------- frontend/udp/connection_id_test.go | 30 ++++----- frontend/udp/frontend.go | 22 ++++++- go.mod | 13 ++-- go.sum | 22 +++---- 5 files changed, 118 insertions(+), 67 deletions(-) diff --git a/frontend/udp/connection_id.go b/frontend/udp/connection_id.go index 8a4546d..cbf5a9a 100644 --- a/frontend/udp/connection_id.go +++ b/frontend/udp/connection_id.go @@ -3,17 +3,17 @@ package udp import ( "crypto/hmac" "encoding/binary" + "github.com/cespare/xxhash/v2" "hash" + "math/rand" "net/netip" "time" - "github.com/zeebo/xxh3" - "github.com/sot-tech/mochi/pkg/log" ) // ttl is the duration a connection ID should be valid according to BEP 15. -const ttl = 2 * time.Minute +var ttl = int64(2 * time.Minute) // A ConnectionIDGenerator is a reusable generator and validator for connection // IDs as described in BEP 15. @@ -33,66 +33,102 @@ type ConnectionIDGenerator struct { // It will be overwritten by subsequent calls to Generate. connID []byte + buff []byte + + // scratch is a 32-byte slice that is used as a scratchpad for the generated + // HMACs. + scratch []byte + // the leeway for a timestamp on a connection ID. - maxClockSkew time.Duration + maxClockSkew int64 + + cnt uint64 } // NewConnectionIDGenerator creates a new connection ID generator. func NewConnectionIDGenerator(key string, maxClockSkew time.Duration) *ConnectionIDGenerator { return &ConnectionIDGenerator{ mac: hmac.New(func() hash.Hash { - return xxh3.New() + return xxhash.New() }, []byte(key)), connID: make([]byte, 8), - maxClockSkew: maxClockSkew, + buff: make([]byte, 9), + scratch: make([]byte, 16), + maxClockSkew: int64(maxClockSkew), + cnt: rand.Uint64(), } } +// xor-shift-star generator +func (g *ConnectionIDGenerator) nextCnt() uint64 { + g.cnt ^= g.cnt >> 12 + g.cnt ^= g.cnt << 25 + g.cnt ^= g.cnt >> 27 + g.cnt *= 0x2545F4914F6CDD1D + return g.cnt +} + +// reset resets the generator. +// This is called by other methods of the generator, it's not necessary to call +// it after getting a generator from a pool. +func (g *ConnectionIDGenerator) reset() { + g.mac.Reset() + g.connID = g.connID[:8] + g.buff = g.buff[:9] + g.scratch = g.scratch[:0] +} + // Generate generates an 8-byte connection ID as described in BEP 15 for the // given IP and the current time. // -// The first 4 bytes of the connection identifier is a unix timestamp and the -// last 4 bytes are a truncated HMAC token created from the aforementioned -// unix timestamp and the source IP address of the UDP packet. +// The first byte is random salt, next 2 bytes - truncated unix timestamp +// when ID was generated, last 5 bytes are a truncated HMAC token created +// from salt (1 byte), full unix timestamp (8 bytes) and source IP (4/16 bytes). +// Salt used to mitigate generation same MAC if there are several clients +// from same IP sent requests within one second. // // Truncated HMAC is known to be safe for 2^(-n) where n is the size in bits -// of the truncated HMAC token. In this use case we have 32 bits, thus a +// of the truncated HMAC token. In this use case we have 40 bits, thus a // forgery probability of approximately 1 in 4 billion. // -// The generated ID is written to g.connID, which is also returned. g.connID +// The generated ID is written to g.buffer, which is also returned. g.buffer // will be reused, so it must not be referenced after returning the generator // to a pool and will be overwritten be subsequent calls to Generate! -func (g *ConnectionIDGenerator) Generate(ip netip.Addr, now time.Time) []byte { - g.mac.Reset() - binary.BigEndian.PutUint32(g.connID, uint32(now.Unix())) - - g.mac.Write(g.connID[:4]) +func (g *ConnectionIDGenerator) Generate(ip netip.Addr, now time.Time) (out []byte) { + g.reset() + g.buff[0] = byte(g.nextCnt()) + binary.BigEndian.PutUint64(g.buff[1:], uint64(now.Unix())) + g.mac.Write(g.buff) g.mac.Write(ip.AsSlice()) - copy(g.connID[4:8], g.mac.Sum(nil)[:4]) + + g.scratch = g.mac.Sum(g.scratch) + g.connID[0], g.connID[1], g.connID[2] = g.buff[0], g.buff[7], g.buff[8] + copy(g.connID[3:], g.scratch[:5]) log.Debug(). Stringer("ip", ip). - Time("now", now). Hex("connID", g.connID). Msg("generated connection ID") - return g.connID + return g.connID[:8] } // Validate validates the given connection ID for an IP and the current time. func (g *ConnectionIDGenerator) Validate(connectionID []byte, ip netip.Addr, now time.Time) bool { - g.mac.Reset() - tsBytes := connectionID[:4] - ts := time.Unix(int64(binary.BigEndian.Uint32(tsBytes)), 0) + g.reset() + nowTS := now.Unix() + g.buff[0] = connectionID[0] + ts := nowTS&((^int64(0)>>16)<<16) | int64(connectionID[1])<<8 | int64(connectionID[2]) + binary.BigEndian.PutUint64(g.buff[1:], uint64(ts)) + g.mac.Write(g.buff) + g.mac.Write(ip.AsSlice()) + g.scratch = g.mac.Sum(g.scratch) + res := hmac.Equal(g.scratch[:5], connectionID[3:8]) + res = ts-g.maxClockSkew <= nowTS && res + res = nowTS < ts+ttl+g.maxClockSkew && res log.Debug(). Stringer("ip", ip). - Time("ts", ts). - Time("now", now). - Hex("connID", g.connID). + Hex("connID", connectionID). + Bool("result", res). Msg("validating connection ID") - - g.mac.Write(tsBytes) - g.mac.Write(ip.AsSlice()) - return hmac.Equal(g.mac.Sum(nil)[:4], connectionID[4:8]) && - now.Before(ts.Add(ttl)) && - ts.Before(now.Add(g.maxClockSkew)) + return res } diff --git a/frontend/udp/connection_id_test.go b/frontend/udp/connection_id_test.go index aa23157..139bf12 100644 --- a/frontend/udp/connection_id_test.go +++ b/frontend/udp/connection_id_test.go @@ -4,16 +4,17 @@ import ( "crypto/hmac" "encoding/binary" "fmt" + "github.com/cespare/xxhash/v2" "hash" + "math/rand" "net/netip" "sync" "testing" "time" - "github.com/stretchr/testify/require" - "github.com/zeebo/xxh3" - "github.com/sot-tech/mochi/pkg/log" + _ "github.com/sot-tech/mochi/pkg/randseed" + "github.com/stretchr/testify/require" ) var golden = []struct { @@ -46,25 +47,26 @@ func ValidConnectionID(connectionID []byte, ip netip.Addr, now time.Time, maxClo // simpleNewConnectionID generates a new connection ID the explicit way. // This is used to verify correct behaviour of the generator. func simpleNewConnectionID(ip netip.Addr, now time.Time, key string) []byte { - buf := make([]byte, 8) - binary.BigEndian.PutUint32(buf, uint32(now.Unix())) - + buffer := make([]byte, 9) mac := hmac.New(func() hash.Hash { - return xxh3.New() + return xxhash.New() }, []byte(key)) - mac.Write(buf[:4]) + buffer[0] = byte(rand.Int()) + binary.BigEndian.PutUint64(buffer[1:], uint64(now.Unix())) + mac.Write(buffer) mac.Write(ip.AsSlice()) - macBytes := mac.Sum(nil)[:4] - copy(buf[4:], macBytes) + buffer[0], buffer[1], buffer[2] = buffer[0], buffer[7], buffer[8] + copy(buffer[3:8], mac.Sum(nil)) + buffer = buffer[:8] // this is just in here because logging impacts performance and we benchmark // this version too. log.Debug(). Stringer("ip", ip). - Time("now", now). - Hex("connID", buf). - Msg("manually generated connection ID") - return buf + Time("ts", now). + Hex("connID", buffer). + Msg("generated connection ID") + return buffer } func TestVerification(t *testing.T) { diff --git a/frontend/udp/frontend.go b/frontend/udp/frontend.go index 7c8a22b..43873fe 100644 --- a/frontend/udp/frontend.go +++ b/frontend/udp/frontend.go @@ -24,8 +24,12 @@ import ( "github.com/sot-tech/mochi/pkg/timecache" ) -// Name - registered name of the frontend -const Name = "udp" +const ( + // Name - registered name of the frontend + Name = "udp" + maxAllowedClockSkew = 30 * time.Second + defaultMaxClockSkew = 10 * time.Second +) var ( logger = log.NewLogger("frontend/udp") @@ -62,7 +66,19 @@ func (cfg Config) Validate() (validCfg Config) { logger.Warn(). Str("name", "PrivateKey"). Str("provided", ""). - Str("key", validCfg.PrivateKey). + Str("default", validCfg.PrivateKey). + Msg("falling back to default configuration") + } + + sb := cfg.MaxClockSkew >> 63 + validCfg.MaxClockSkew = (cfg.MaxClockSkew ^ sb) + (sb & 1) + + if validCfg.MaxClockSkew == 0 || validCfg.MaxClockSkew > maxAllowedClockSkew { + validCfg.MaxClockSkew = defaultMaxClockSkew + logger.Warn(). + Str("name", "MaxClockSkew"). + Dur("provided", cfg.MaxClockSkew). + Dur("default", validCfg.MaxClockSkew). Msg("falling back to default configuration") } diff --git a/go.mod b/go.mod index ab9a59e..6d84fbb 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,12 @@ module github.com/sot-tech/mochi go 1.19 require ( - code.cloudfoundry.org/go-diodes v0.0.0-20221122215814-f99b6a62c703 - github.com/MicahParks/keyfunc v1.6.0 - github.com/anacrolix/torrent v1.47.0 + code.cloudfoundry.org/go-diodes v0.0.0-20221128154616-f0ef2e038721 + github.com/MicahParks/keyfunc v1.7.0 + github.com/anacrolix/torrent v1.47.1-0.20221102120345-c63f7e1bd720 + github.com/cespare/xxhash/v2 v2.1.2 github.com/go-redis/redis/v8 v8.11.5 - github.com/golang-jwt/jwt/v4 v4.4.2 + github.com/golang-jwt/jwt/v4 v4.4.3 github.com/jackc/pgx/v5 v5.1.1 github.com/julienschmidt/httprouter v1.3.0 github.com/libp2p/go-reuseport v0.2.0 @@ -16,18 +17,16 @@ require ( github.com/prometheus/client_golang v1.14.0 github.com/rs/zerolog v1.28.0 github.com/stretchr/testify v1.8.1 - github.com/zeebo/xxh3 v1.0.2 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/anacrolix/dht/v2 v2.19.1 // indirect + github.com/anacrolix/dht/v2 v2.19.2 // indirect github.com/anacrolix/log v0.13.2-0.20220711050817-613cb738ef30 // indirect github.com/anacrolix/missinggo v1.3.0 // indirect github.com/anacrolix/missinggo/v2 v2.7.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect diff --git a/go.sum b/go.sum index 97cd23c..4530af9 100644 --- a/go.sum +++ b/go.sum @@ -30,15 +30,15 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -code.cloudfoundry.org/go-diodes v0.0.0-20221122215814-f99b6a62c703 h1:AYI2TSzZnOwbxvCy1a2+AINk2d43J8+z4wVxt6YC364= -code.cloudfoundry.org/go-diodes v0.0.0-20221122215814-f99b6a62c703/go.mod h1:kDqUxF0g8gENeHnd/Np+/ES9u2l00Maamk2ECzPd+lA= +code.cloudfoundry.org/go-diodes v0.0.0-20221128154616-f0ef2e038721 h1:Dr3lCN4tcyMoX04sUwP9C0uCO5Uc+J11GWGVBNIJaYk= +code.cloudfoundry.org/go-diodes v0.0.0-20221128154616-f0ef2e038721/go.mod h1:kDqUxF0g8gENeHnd/Np+/ES9u2l00Maamk2ECzPd+lA= crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/MicahParks/keyfunc v1.6.0 h1:VsoMTaQDCxaEYhlYVmuqkOopsDx12/XqIgJIz7cl/0g= -github.com/MicahParks/keyfunc v1.6.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= +github.com/MicahParks/keyfunc v1.7.0 h1:LBd4tBj6FwGs2S4GXniQbgrG0PXzIldyGDKWch8slhg= +github.com/MicahParks/keyfunc v1.7.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= @@ -49,8 +49,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/anacrolix/dht/v2 v2.19.1 h1:V/UUGBASGYqYkSnmHJwX8uQmzkyhbgwE6jqcHKnNTD8= -github.com/anacrolix/dht/v2 v2.19.1/go.mod h1:3TU93c1s/oA8I/VH4m3CNP/BeKsiOGmo6HwfZBMTKUs= +github.com/anacrolix/dht/v2 v2.19.2 h1:U+lbEFYwa9yajIW2oE9jBxt0K8DlT10pSTYbXyfCxC8= +github.com/anacrolix/dht/v2 v2.19.2/go.mod h1:MctKM1HS5YYDb3F30NGJxLE+QPuqWoT5ReW/4jt8xew= github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= @@ -73,8 +73,8 @@ github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQ github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= -github.com/anacrolix/torrent v1.47.0 h1:aDUnhQZ8+kfStLICHiXOGGYVFgDENK+kz4q96linyRg= -github.com/anacrolix/torrent v1.47.0/go.mod h1:SYPxEUjMwqhDr3kWGzyQLkFMuAb1bgJ57JRMpuD3ZzE= +github.com/anacrolix/torrent v1.47.1-0.20221102120345-c63f7e1bd720 h1:3cvS7QtC2hcrIOfT44fKDfPZUcXvkDf9SkI1mN6TB+s= +github.com/anacrolix/torrent v1.47.1-0.20221102120345-c63f7e1bd720/go.mod h1:SYPxEUjMwqhDr3kWGzyQLkFMuAb1bgJ57JRMpuD3ZzE= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -141,8 +141,9 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -367,9 +368,6 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= -github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=