From c66f8695810220aa76a52012df52b0312d745086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 30 Apr 2022 10:31:12 +0800 Subject: [PATCH] Add simple proxy portal --- cli/portal/portal-v2board/api.go | 287 +++++++++++++++++++++++++++ cli/portal/portal-v2board/config.go | 24 +++ cli/portal/portal-v2board/main.go | 237 ++++++++++++++++++++++ cli/portal/portal-v2board/traffic.go | 134 +++++++++++++ go.mod | 1 + go.sum | 8 + 6 files changed, 691 insertions(+) create mode 100644 cli/portal/portal-v2board/api.go create mode 100644 cli/portal/portal-v2board/config.go create mode 100644 cli/portal/portal-v2board/main.go create mode 100644 cli/portal/portal-v2board/traffic.go diff --git a/cli/portal/portal-v2board/api.go b/cli/portal/portal-v2board/api.go new file mode 100644 index 0000000..3d55d60 --- /dev/null +++ b/cli/portal/portal-v2board/api.go @@ -0,0 +1,287 @@ +package main + +import ( + "context" + "encoding/json" + + "github.com/go-resty/resty/v2" + E "github.com/sagernet/sing/common/exceptions" +) + +type NodeClient struct { + client *resty.Client +} + +func NewNodeClient(baseURL string, token string, node string) *NodeClient { + r := resty.New() + r.SetBaseURL(baseURL) + r.SetQueryParams(map[string]string{ + "node_id": node, + "token": token, + }) + // r.SetDebug(true) + return &NodeClient{r} +} + +type ShadowsocksUserList struct { + Port uint16 + Method string + Users map[int]string // id password +} + +type RawShadowsocksUserList struct { + Data []RawShadowsocksUser `json:"data"` +} + +type RawShadowsocksUser struct { + Id int `json:"id"` + Port uint16 `json:"port"` + Cipher string `json:"cipher"` + Secret string `json:"secret"` +} + +func (c *NodeClient) GetShadowsocksUserList(ctx context.Context) (*ShadowsocksUserList, error) { + resp, err := c.client.R(). + SetContext(ctx). + SetResult(new(RawShadowsocksUserList)). + Get("/api/v1/server/ShadowsocksTidalab/user") + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, E.New("HTTP ", resp.StatusCode(), " ", resp.Body()) + } + + rawUserList := resp.Result().(*RawShadowsocksUserList) + + userList := &ShadowsocksUserList{ + Method: rawUserList.Data[0].Cipher, + Port: rawUserList.Data[0].Port, + Users: make(map[int]string), + } + + for _, item := range rawUserList.Data { + if item.Cipher != userList.Method { + return nil, E.New("not unique method in item ", item.Id) + } + if item.Port != userList.Port { + return nil, E.New("not unique port in item ", item.Id) + } + userList.Users[item.Id] = item.Secret + } + + return userList, nil +} + +type TrojanUserList struct { + Users map[int]string // id password +} + +type RawTrojanUserList struct { + Msg string `json:"msg"` + Data []RawTrojanUser `json:"data"` +} + +type RawTrojanUser struct { + ID int `json:"id"` + T int `json:"t"` + U int64 `json:"u"` + D int64 `json:"d"` + TransferEnable int64 `json:"transfer_enable"` + TrojanUser struct { + Password string `json:"password"` + } `json:"trojan_user"` +} + +func (c *NodeClient) GetTrojanUserList(ctx context.Context) (*TrojanUserList, error) { + resp, err := c.client.R(). + SetContext(ctx). + SetResult(new(RawTrojanUserList)). + Get("/api/v1/server/TrojanTidalab/user") + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, E.New("HTTP ", resp.StatusCode(), " ", resp.String()) + } + + rawUserList := resp.Result().(*RawTrojanUserList) + + userList := &TrojanUserList{ + Users: make(map[int]string), + } + + for _, item := range rawUserList.Data { + userList.Users[item.ID] = item.TrojanUser.Password + } + + return userList, nil +} + +type TrojanConfig struct { + LocalPort uint16 + SNI string +} + +type RawTrojanConfig struct { + LocalPort uint16 `json:"local_port"` + Ssl struct { + Sni string `json:"sni"` + } `json:"ssl"` +} + +func (c *NodeClient) GetTrojanConfig(ctx context.Context) (*TrojanConfig, error) { + resp, err := c.client.R(). + SetContext(ctx). + SetQueryParam("local_port", "1"). + Get("/api/v1/server/TrojanTidalab/config") + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, E.New("HTTP ", resp.StatusCode(), " ", resp.String()) + } + + rawConfig := new(RawTrojanConfig) + err = json.Unmarshal(resp.Body(), rawConfig) + if err != nil { + return nil, E.Cause(err, "parse raw trojan config") + } + + trojanConfig := new(TrojanConfig) + trojanConfig.LocalPort = rawConfig.LocalPort + trojanConfig.SNI = rawConfig.Ssl.Sni + + return trojanConfig, nil +} + +type VMessUserList struct { + AlterID int + Users map[int]string // id uuid +} + +type RawVMessUserList struct { + Msg string `json:"msg"` + Data []RawVMessUser `json:"data"` +} + +type RawVMessUser struct { + Id int `json:"id"` + T int `json:"t"` + U int64 `json:"u"` + D int64 `json:"d"` + TransferEnable int64 `json:"transfer_enable"` + V2RayUser struct { + Uuid string `json:"uuid"` + Email string `json:"email"` + AlterId int `json:"alter_id"` + Level int `json:"level"` + } `json:"v2ray_user"` +} + +func (c *NodeClient) GetVMessUserList(ctx context.Context) (*VMessUserList, error) { + resp, err := c.client.R(). + SetContext(ctx). + SetResult(new(RawVMessUserList)). + Get("/api/v1/server/Deepbwork/user") + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, E.New("HTTP ", resp.StatusCode(), " ", resp.String()) + } + + rawUserList := resp.Result().(*RawVMessUserList) + + userList := &VMessUserList{ + AlterID: rawUserList.Data[0].V2RayUser.AlterId, + Users: make(map[int]string), + } + + for _, user := range rawUserList.Data { + userList.Users[user.Id] = user.V2RayUser.Uuid + } + + return userList, nil +} + +type VMessConfig struct { + Port uint16 + Network string + Security string +} + +type RawV2RayConfig struct { + Inbounds []struct { + Protocol string `json:"protocol"` + Port uint16 `json:"port"` + StreamSettings struct { + Network string `json:"network"` + Security string `json:"security,omitempty"` + } `json:"streamSettings,omitempty"` + } `json:"inbounds"` +} + +func (c *NodeClient) GetVMessConfig(ctx context.Context) (*VMessConfig, error) { + resp, err := c.client.R(). + SetContext(ctx). + SetQueryParam("local_port", "1"). + Get("/api/v1/server/Deepbwork/config") + if err != nil { + return nil, err + } + + if !resp.IsSuccess() { + return nil, E.New("HTTP ", resp.StatusCode(), " ", resp.String()) + } + + rawConfig := new(RawV2RayConfig) + err = json.Unmarshal(resp.Body(), rawConfig) + if err != nil { + return nil, err + } + + vmessConfig := new(VMessConfig) + vmessConfig.Port = rawConfig.Inbounds[0].Port + vmessConfig.Network = rawConfig.Inbounds[0].StreamSettings.Network + vmessConfig.Security = rawConfig.Inbounds[0].StreamSettings.Security + + return vmessConfig, nil +} + +type UserTraffic struct { + UID int `json:"user_id"` + Upload int64 `json:"u"` + Download int64 `json:"d"` +} + +func (c *NodeClient) ReportShadowsocksTraffic(ctx context.Context, userTraffic []UserTraffic) error { + return c.reportTraffic(ctx, "/api/v1/server/ShadowsocksTidalab/submit", userTraffic) +} + +func (c *NodeClient) ReportVMessTraffic(ctx context.Context, userTraffic []UserTraffic) error { + return c.reportTraffic(ctx, "/api/v1/server/Deepbwork/submit", userTraffic) +} + +func (c *NodeClient) ReportTrojanTraffic(ctx context.Context, userTraffic []UserTraffic) error { + return c.reportTraffic(ctx, "/api/v1/server/TrojanTidalab/submit", userTraffic) +} + +func (c *NodeClient) reportTraffic(ctx context.Context, path string, userTraffic []UserTraffic) error { + resp, err := c.client.R(). + SetContext(ctx). + SetBody(userTraffic). + Post(path) + if err != nil { + return err + } + + if !resp.IsSuccess() { + return E.New("HTTP ", resp.StatusCode(), " ", resp.String()) + } + return nil +} diff --git a/cli/portal/portal-v2board/config.go b/cli/portal/portal-v2board/config.go new file mode 100644 index 0000000..c518181 --- /dev/null +++ b/cli/portal/portal-v2board/config.go @@ -0,0 +1,24 @@ +package main + +import "encoding/json" + +type Config struct { + URL string `json:"url"` + Token string `json:"token"` + Nodes []Node `json:"nodes"` + TLS *TLSConfig `json:"tls,omitempty"` + Debug bool `json:"debug"` +} + +type Node struct { + ID int `json:"id"` + Type string `json:"type"` + Domain string `json:"domain"` +} + +type TLSConfig struct { + Insecure bool `json:"insecure"` + Email string `json:"email"` + DNSProvider string `json:"dns_provider"` + DNSEnv json.RawMessage `json:"dns_env"` +} diff --git a/cli/portal/portal-v2board/main.go b/cli/portal/portal-v2board/main.go new file mode 100644 index 0000000..38a0d22 --- /dev/null +++ b/cli/portal/portal-v2board/main.go @@ -0,0 +1,237 @@ +package main + +import ( + "context" + "crypto/tls" + "encoding/json" + "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" + "io/ioutil" + "net" + "os" + "os/signal" + "strconv" + "syscall" + "time" + + "github.com/sagernet/sing" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/random" + "github.com/sagernet/sing/protocol/socks" + "github.com/sagernet/sing/protocol/trojan" + transTLS "github.com/sagernet/sing/transport/tls" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var configPath string + +func main() { + command := &cobra.Command{ + Use: "portal-v2board [-c config.json]", + Args: cobra.NoArgs, + Version: sing.VersionStr, + Run: run, + } + + command.Flags().StringVarP(&configPath, "config", "c", "config.json", "set config path") + + if err := command.Execute(); err != nil { + logrus.Fatal(err) + } +} + +func run(cmd *cobra.Command, args []string) { + data, err := ioutil.ReadFile(configPath) + if err != nil { + logrus.Fatal(E.Cause(err, "read config")) + } + config := new(Config) + err = json.Unmarshal(data, config) + if err != nil { + logrus.Fatal(E.Cause(err, "parse config")) + } + if config.Debug { + logrus.SetLevel(logrus.TraceLevel) + } + if len(config.Nodes) == 0 { + logrus.Fatal("empty nodes") + } + var instances []Instance + for _, node := range config.Nodes { + client := NewNodeClient(config.URL, config.Token, strconv.Itoa(node.ID)) + switch node.Type { + case "trojan": + instances = append(instances, NewTrojanInstance(client, node)) + default: + logrus.Fatal("unsupported node type ", node.Type, " (id: ", node.ID, ")") + } + } + for _, instance := range instances { + err = instance.Start() + if err != nil { + logrus.Fatal(err) + } + } + + osSignals := make(chan os.Signal, 1) + signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM) + <-osSignals + + for _, instance := range instances { + instance.Close() + } +} + +type Instance interface { + Start() error + Close() error +} + +type TrojanInstance struct { + *NodeClient + id int + domain string + listener net.Listener + service trojan.Service[int] + user UserManager + reloadTicker *time.Ticker +} + +func NewTrojanInstance(client *NodeClient, node Node) *TrojanInstance { + t := &TrojanInstance{ + NodeClient: client, + id: node.ID, + domain: node.Domain, + user: NewUserManager(), + } + t.service = trojan.NewService[int](t) + return t +} + +func (i *TrojanInstance) Start() error { + err := i.reloadUsers() + if err != nil { + return err + } + + trojanConfig, err := i.GetTrojanConfig(context.Background()) + if err != nil { + return E.CauseF(err, i.id, ": read trojan config") + } + + tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{ + Port: int(trojanConfig.LocalPort), + }) + if err != nil { + return E.CauseF(err, i.id, ": listen at tcp:", trojanConfig.LocalPort, ", check server configuration!") + } + + i.listener = tls.NewListener(tcpListener, &tls.Config{ + Rand: random.Blake3KeyedHash(), + GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + return transTLS.GenerateCertificate(info.ServerName) + }, + }) + + logrus.Info(i.id, ": started at ", tcpListener.Addr()) + go i.loopRequests() + + i.reloadTicker = time.NewTicker(time.Minute) + go i.loopReload() + return nil +} + +func (i *TrojanInstance) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + userCtx := ctx.(*trojan.Context[int]) + conn = i.user.TrackConnection(userCtx.User, conn) + logrus.Info(i.id, ": user ", userCtx.User, " TCP ", metadata.Source, " ==> ", metadata.Destination) + destConn, err := network.SystemDialer.DialContext(context.Background(), "tcp", metadata.Destination) + if err != nil { + return err + } + return rw.CopyConn(ctx, conn, destConn) +} + +func (i *TrojanInstance) NewPacketConnection(ctx context.Context, conn socks.PacketConn, metadata M.Metadata) error { + userCtx := ctx.(*trojan.Context[int]) + conn = i.user.TrackPacketConnection(userCtx.User, conn) + logrus.Info(i.id, ": user ", userCtx.User, " UDP ", metadata.Source, " ==> ", metadata.Destination) + udpConn, err := net.ListenUDP("udp", nil) + if err != nil { + return err + } + return socks.CopyNetPacketConn(ctx, udpConn, conn) +} + +func (i *TrojanInstance) loopRequests() { + for { + conn, err := i.listener.Accept() + if err != nil { + logrus.Debug(E.CauseF(err, i.id, ": listener exited")) + return + } + go func() { + hErr := i.service.NewConnection(context.Background(), conn, M.Metadata{ + Protocol: "tls", + Source: M.AddrPortFromNetAddr(conn.RemoteAddr()), + }) + if hErr != nil { + i.HandleError(hErr) + } + }() + } +} + +func (i *TrojanInstance) loopReload() { + for range i.reloadTicker.C { + err := i.reloadUsers() + if err != nil { + i.HandleError(E.CauseF(err, "reload user")) + } + traffics := i.user.ReadTraffics() + if len(traffics) > 0 { + err = i.ReportTrojanTraffic(context.Background(), traffics) + if err != nil { + i.HandleError(E.CauseF(err, "report traffic")) + } + } + } +} + +func (i *TrojanInstance) reloadUsers() error { + logrus.Debug(i.id, ": fetching users...") + userList, err := i.GetTrojanUserList(context.Background()) + if err != nil { + return E.CauseF(err, i.id, ": get user list") + } + if len(userList.Users) == 0 { + logrus.Warn(i.id, ": empty users") + } + + i.service.ResetUsers() + for id, password := range userList.Users { + err = i.service.AddUser(id, password) + if err != nil { + logrus.Warn(E.CauseF(err, i.id, ": add user")) + } + } + + logrus.Debug(i.id, ": loaded ", len(userList.Users), " users") + return nil +} + +func (i *TrojanInstance) HandleError(err error) { + common.Close(err) + if E.IsClosed(err) { + return + } + logrus.Warn(i.id, ": ", err) +} + +func (i *TrojanInstance) Close() error { + i.reloadTicker.Stop() + return i.listener.Close() +} diff --git a/cli/portal/portal-v2board/traffic.go b/cli/portal/portal-v2board/traffic.go new file mode 100644 index 0000000..2149c23 --- /dev/null +++ b/cli/portal/portal-v2board/traffic.go @@ -0,0 +1,134 @@ +package main + +import ( + "io" + "net" + "sync" + "sync/atomic" + + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/protocol/socks" +) + +type UserManager struct { + access sync.Mutex + users map[int]*User +} + +type User struct { + Upload uint64 + Download uint64 +} + +func NewUserManager() UserManager { + return UserManager{ + users: make(map[int]*User), + } +} + +func (m *UserManager) TrackConnection(userId int, conn net.Conn) net.Conn { + m.access.Lock() + defer m.access.Unlock() + var user *User + if u, loaded := m.users[userId]; loaded { + user = u + } else { + user = new(User) + m.users[userId] = user + } + return &TrackConn{conn, user} +} + +func (m *UserManager) TrackPacketConnection(userId int, conn socks.PacketConn) socks.PacketConn { + m.access.Lock() + defer m.access.Unlock() + var user *User + if u, loaded := m.users[userId]; loaded { + user = u + } else { + user = new(User) + m.users[userId] = user + } + return &TrackPacketConn{conn, user} +} + +func (m *UserManager) ReadTraffics() []UserTraffic { + m.access.Lock() + defer m.access.Unlock() + + traffic := make([]UserTraffic, 0, len(m.users)) + for userId, user := range m.users { + upload := atomic.SwapUint64(&user.Upload, 0) + download := atomic.SwapUint64(&user.Download, 0) + if upload == 0 && download == 0 { + continue + } + traffic = append(traffic, UserTraffic{ + UID: userId, + Upload: int64(upload), + Download: int64(download), + }) + } + + return traffic +} + +type TrackConn struct { + net.Conn + *User +} + +func (c *TrackConn) Read(p []byte) (n int, err error) { + n, err = c.Conn.Read(p) + if n > 0 { + atomic.AddUint64(&c.Upload, uint64(n)) + } + return +} + +func (c *TrackConn) Write(p []byte) (n int, err error) { + n, err = c.Conn.Write(p) + if n > 0 { + atomic.AddUint64(&c.Download, uint64(n)) + } + return +} + +func (c *TrackConn) WriteTo(w io.Writer) (n int64, err error) { + n, err = io.Copy(w, c.Conn) + if n > 0 { + atomic.AddUint64(&c.Upload, uint64(n)) + } + return +} + +func (c *TrackConn) ReadFrom(r io.Reader) (n int64, err error) { + n, err = io.Copy(c.Conn, r) + if n > 0 { + atomic.AddUint64(&c.Download, uint64(n)) + } + return +} + +type TrackPacketConn struct { + socks.PacketConn + *User +} + +func (c *TrackPacketConn) ReadPacket(buffer *buf.Buffer) (*M.AddrPort, error) { + destination, err := c.PacketConn.ReadPacket(buffer) + if err == nil { + atomic.AddUint64(&c.Upload, uint64(buffer.Len())) + } + return destination, err +} + +func (c *TrackPacketConn) WritePacket(buffer *buf.Buffer, destination *M.AddrPort) error { + n := buffer.Len() + err := c.PacketConn.WritePacket(buffer, destination) + if err == nil { + atomic.AddUint64(&c.Download, uint64(n)) + } + return err +} diff --git a/go.mod b/go.mod index e4ca960..1aec491 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/cloudflare/cloudflare-go v0.38.0 + github.com/go-resty/resty/v2 v2.7.0 github.com/openacid/low v0.1.21 github.com/oschwald/geoip2-golang v1.7.0 github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb diff --git a/go.sum b/go.sum index 81212fe..61aebe6 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0= github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= @@ -73,17 +75,23 @@ github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695AP github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d h1:q4JksJ2n0fmbXC0Aj0eOs6E0AcPqnKglxWXWFqGD6x0=