diff options
| author | Xe Iaso <me@xeiaso.net> | 2025-04-27 12:35:45 -0400 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2025-04-27 12:42:58 -0400 |
| commit | ef94cbcc7f9f90ef5c238413ee3305c305743a42 (patch) | |
| tree | c41253eefe23a91b83cec041c9ee0e86bac53da5 | |
| parent | 972cc990716c8593fc1f1d7061e6b707c6bccc51 (diff) | |
| download | x-ef94cbcc7f9f90ef5c238413ee3305c305743a42.tar.xz x-ef94cbcc7f9f90ef5c238413ee3305c305743a42.zip | |
feat(relayd): store and query TLS fingerprints
Release-Status: cut
Signed-off-by: Xe Iaso <me@xeiaso.net>
| -rw-r--r-- | cmd/relayd/main.go | 93 | ||||
| -rw-r--r-- | cmd/relayd/relayd.service | 6 | ||||
| -rw-r--r-- | go.mod | 13 | ||||
| -rw-r--r-- | go.sum | 22 |
4 files changed, 127 insertions, 7 deletions
diff --git a/cmd/relayd/main.go b/cmd/relayd/main.go index 3185b37..a658673 100644 --- a/cmd/relayd/main.go +++ b/cmd/relayd/main.go @@ -3,6 +3,8 @@ package main import ( "context" "crypto/tls" + "database/sql" + "errors" "flag" "fmt" "log" @@ -19,16 +21,20 @@ import ( "syscall" "time" + "github.com/avct/uasurfer" "github.com/google/uuid" + "github.com/jackc/puddle/v2" + _ "modernc.org/sqlite" "within.website/x/internal" ) var ( - bind = flag.String("bind", ":3004", "port to listen on") - certDir = flag.String("cert-dir", "/xe/pki", "where to read mounted certificates from") - certFname = flag.String("cert-fname", "tls.crt", "certificate filename") - keyFname = flag.String("key-fname", "tls.key", "key filename") - proxyTo = flag.String("proxy-to", "http://localhost:5000", "where to reverse proxy to") + bind = flag.String("bind", ":3004", "port to listen on") + certDir = flag.String("cert-dir", "/xe/pki", "where to read mounted certificates from") + certFname = flag.String("cert-fname", "tls.crt", "certificate filename") + fpDatabase = flag.String("fp-database", "", "location of fingerprint database") + keyFname = flag.String("key-fname", "tls.key", "key filename") + proxyTo = flag.String("proxy-to", "http://localhost:5000", "where to reverse proxy to") ) func main() { @@ -38,6 +44,7 @@ func main() { "bind", *bind, "cert-dir", *certDir, "cert-fname", *certFname, + "fp-database", *fpDatabase, "key-fname", *keyFname, "proxy-to", *proxyTo, ) @@ -82,6 +89,24 @@ func main() { log.Fatal(err) } + var db *sql.DB + + if fpDatabase != nil { + var err error + db, err = sql.Open("sqlite", *fpDatabase) + if err != nil { + log.Fatal(err) + } + if err := db.Ping(); err != nil { + log.Fatal(err) + } + } + + uaParser, err := uaParserPool() + if err != nil { + log.Fatal(err) + } + h := httputil.NewSingleHostReverseProxy(u) oldDirector := h.Director @@ -117,6 +142,47 @@ func main() { // req.Header.Set("X-TCP-Fingerprint-JA4T", tcpFP.String()) // } + if uap, err := uaParser.Acquire(req.Context()); err == nil { + defer uap.Release() + + uasurfer.ParseUserAgent(req.UserAgent(), uap.Value()) + + vers := uap.Value().Browser.Version + + req.Header.Set("Xe-X-Relayd-UserAgent-Browser", uap.Value().Browser.Name.StringTrimPrefix()) + req.Header.Set("Xe-X-Relayd-UserAgent-Browser-Version", fmt.Sprintf("%d.%d.%d", vers.Major, vers.Minor, vers.Patch)) + req.Header.Set("Xe-X-Relayd-UserAgent-OS", uap.Value().OS.Name.StringTrimPrefix()) + req.Header.Set("Xe-X-Relayd-UserAgent-Platform", uap.Value().OS.Platform.StringTrimPrefix()) + } + + if ja4 := req.Header.Get("X-TLS-Fingerprint-JA4"); db != nil && ja4 != "" { + var application, userAgent, notes sql.NullString + if err := db.QueryRowContext(req.Context(), "SELECT application, user_agent_string, notes FROM fingerprints WHERE ja4_fingerprint = ?", ja4).Scan(&application, &userAgent, ¬es); err == nil { + slog.Debug("found a hit", "application", application, "userAgent", userAgent, "notes", notes) + if application.Valid { + req.Header.Set("Xe-X-Relayd-Ja4-Application", application.String) + } + + if userAgent.Valid { + req.Header.Set("Xe-X-Relayd-Ja4-UserAgent", userAgent.String) + } + + if notes.Valid { + req.Header.Set("Xe-X-Relayd-Ja4-Notes", notes.String) + } + } else if errors.Is(err, sql.ErrNoRows) { + userAgent := req.UserAgent() + notes := fmt.Sprintf("Observed via relayd on host %s at %s", req.Host, time.Now().Format(time.RFC3339)) + if _, err := db.ExecContext(req.Context(), "INSERT INTO fingerprints(user_agent_string, notes, ja4_fingerprint) VALUES (?, ?, ?)", userAgent, notes, ja4); err != nil { + slog.Error("can't insert fingerprint into database", "err", err) + } + + req.Header.Set("Xe-X-Relayd-New-Client", "true") + } else { + slog.Debug("can't read from database", "err", err) + } + } + req.Header.Set("X-Forwarded-Host", req.URL.Host) req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Scheme", "https") @@ -215,3 +281,20 @@ func (kpr *keypairReloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certifica return kpr.cert, nil } + +func uaParserPool() (*puddle.Pool[*uasurfer.UserAgent], error) { + cons := func(context.Context) (*uasurfer.UserAgent, error) { + return &uasurfer.UserAgent{}, nil + } + des := func(ua *uasurfer.UserAgent) { + ua.Reset() + } + + pool, err := puddle.NewPool(&puddle.Config[*uasurfer.UserAgent]{ + Constructor: cons, + Destructor: des, + MaxSize: 512, + }) + + return pool, err +} diff --git a/cmd/relayd/relayd.service b/cmd/relayd/relayd.service index cb6fc07..519d36a 100644 --- a/cmd/relayd/relayd.service +++ b/cmd/relayd/relayd.service @@ -7,6 +7,12 @@ Restart=always RestartSec=30s EnvironmentFile=/etc/within.website/x/relayd.env LimitNOFILE=infinity +DynamicUser=true +CacheDirectory=relayd +CacheDirectoryMode=0755 +StateDirectory=relayd +StateDirectoryMode=0755 +ReadWritePaths=/run [Install] WantedBy=multi-user.target
\ No newline at end of file @@ -90,6 +90,7 @@ require ( github.com/TecharoHQ/yeet v0.2.1 // indirect github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect github.com/andybalholm/brotli v1.1.1 // indirect + github.com/avct/uasurfer v0.0.0-20250320214457-f1f6afeb74db // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.29 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect @@ -109,6 +110,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.8.2 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/evanw/esbuild v0.19.11 // indirect @@ -128,7 +130,7 @@ require ( github.com/goccy/go-yaml v1.12.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a // indirect github.com/goreleaser/chglog v0.7.0 // indirect github.com/goreleaser/fileglob v1.3.0 // indirect @@ -152,6 +154,7 @@ require ( github.com/ipfs/go-metrics-interface v0.0.1 // indirect github.com/ipld/go-car/v2 v2.13.1 // indirect github.com/ipld/go-ipld-prime v0.21.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/goprocess v0.1.4 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -183,12 +186,14 @@ require ( github.com/natefinch/atomic v1.0.1 // indirect github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/term v1.2.0-beta.2 // indirect github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect @@ -222,6 +227,10 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.2.1 // indirect + modernc.org/libc v1.62.1 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.9.1 // indirect + modernc.org/sqlite v1.37.0 // indirect ) require ( @@ -263,7 +272,7 @@ require ( github.com/sacOO7/go-logger v0.0.0-20180719173527-9ac9add5a50d // indirect github.com/sacOO7/gowebsocket v0.0.0-20221109081133-70ac927be105 github.com/sendgrid/rest v2.6.9+incompatible // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 golang.org/x/image v0.24.0 golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.39.0 @@ -119,6 +119,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/avct/uasurfer v0.0.0-20250320214457-f1f6afeb74db h1:Bk7CPE8ZVhOKA11SZlplKBRWfo17OyyPj0t1dgYN2qI= +github.com/avct/uasurfer v0.0.0-20250320214457-f1f6afeb74db/go.mod h1:s+GCtuP4kZNxh1WGoqdWI1+PbluBcycrMMWuKQ9e5Nk= github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0= @@ -245,6 +247,8 @@ github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yA github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c h1:mxWGS0YyquJ/ikZOjSrRjjFIbUqIP9ojyYQ+QZTU3Rg= github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eaburns/peggy v1.0.2 h1:RJwNVF4cvzLGiKInHGBT8sVwP5HDuVP/PaBU9tHO9Rk= github.com/eaburns/peggy v1.0.2/go.mod h1:X2pbl0EV5erfnK8kSGwo0lBCgMGokvR1E6KerAoDKXg= github.com/eaburns/pretty v1.0.0 h1:00W1wrrtMXUSqLPN0txS8j7g9qFXy6nA5vZVqVQOo6w= @@ -442,6 +446,8 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a h1:JJBdjSfqSy3mnDT0940ASQFghwcZ4y4cb6ttjAoXqwE= github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI= @@ -548,6 +554,8 @@ github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo= github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd/go.mod h1:wZ8hH8UxeryOs4kJEJaiui/s00hDSbE37OKsL47g+Sw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= @@ -692,6 +700,8 @@ github.com/ncruces/go-sqlite3 v0.16.0 h1:O7eULuEjvSBnS1QCN+dDL/ixLQZoUGWr466A02G github.com/ncruces/go-sqlite3 v0.16.0/go.mod h1:2TmAeD93ImsKXJRsUIKohfMvt17dZSbS6pzJ3k6YYFg= github.com/ncruces/go-sqlite3/gormlite v0.16.0 h1:pBN2h323adfTF3NThIuHa3e7GCesgqrDZFEdAmdFNQE= github.com/ncruces/go-sqlite3/gormlite v0.16.0/go.mod h1:TyfqjFmR31x1iOth4t6hPgtUf2UvM8zupKSj6TZkP4c= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/nicklaw5/helix/v2 v2.31.0 h1:/8E5H20D/f3PGmSWT5NWtjwt+M8/GeCjnK/AkoLIFQA= @@ -769,6 +779,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1FM= github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -969,6 +981,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1498,6 +1512,14 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= +modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= +modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= +modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= |
