From 5547895dcb9ee8e305a30a17b5de34b3c5da4f07 Mon Sep 17 00:00:00 2001 From: Toby Date: Thu, 9 Apr 2020 14:07:31 -0700 Subject: [PATCH] First commit, forwarder is essentially complete --- .gitignore | 183 +++++++++++++++++++++++++++ cmd/forwarder/client.go | 169 +++++++++++++++++++++++++ cmd/forwarder/config.go | 76 +++++++++++ cmd/forwarder/flags.go | 40 ++++++ cmd/forwarder/main.go | 26 ++++ cmd/forwarder/server.go | 204 ++++++++++++++++++++++++++++++ go.mod | 9 ++ go.sum | 211 +++++++++++++++++++++++++++++++ internal/forwarder/client.go | 200 +++++++++++++++++++++++++++++ internal/forwarder/control.go | 67 ++++++++++ internal/forwarder/control.pb.go | 206 ++++++++++++++++++++++++++++++ internal/forwarder/control.proto | 19 +++ internal/forwarder/params.go | 10 ++ internal/forwarder/protogen.go | 3 + internal/forwarder/server.go | 173 +++++++++++++++++++++++++ internal/forwarder/types.go | 21 +++ internal/utils/pipe.go | 25 ++++ pkg/congestion/brutal.go | 70 ++++++++++ pkg/forwarder/client.go | 70 ++++++++++ pkg/forwarder/interface.go | 89 +++++++++++++ pkg/forwarder/params.go | 9 ++ pkg/forwarder/server.go | 119 +++++++++++++++++ 22 files changed, 1999 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/forwarder/client.go create mode 100644 cmd/forwarder/config.go create mode 100644 cmd/forwarder/flags.go create mode 100644 cmd/forwarder/main.go create mode 100644 cmd/forwarder/server.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/forwarder/client.go create mode 100644 internal/forwarder/control.go create mode 100644 internal/forwarder/control.pb.go create mode 100644 internal/forwarder/control.proto create mode 100644 internal/forwarder/params.go create mode 100644 internal/forwarder/protogen.go create mode 100644 internal/forwarder/server.go create mode 100644 internal/forwarder/types.go create mode 100644 internal/utils/pipe.go create mode 100644 pkg/congestion/brutal.go create mode 100644 pkg/forwarder/client.go create mode 100644 pkg/forwarder/interface.go create mode 100644 pkg/forwarder/params.go create mode 100644 pkg/forwarder/server.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba0b0dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,183 @@ +# Created by https://www.gitignore.io/api/go,linux,macos,windows,intellij+all +# Edit at https://www.gitignore.io/?templates=go,linux,macos,windows,intellij+all + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +### Go Patch ### +/vendor/ +/Godeps/ + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/go,linux,macos,windows,intellij+all + +cmd/forwarder/*.json +cmd/forwarder/forwarder diff --git a/cmd/forwarder/client.go b/cmd/forwarder/client.go new file mode 100644 index 0000000..0e07e75 --- /dev/null +++ b/cmd/forwarder/client.go @@ -0,0 +1,169 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "flag" + "fmt" + "github.com/lucas-clemente/quic-go/congestion" + hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion" + "github.com/tobyxdd/hysteria/pkg/forwarder" + "io/ioutil" + "log" + "net" + "os" + "os/user" +) + +func loadCmdClientConfig(args []string) (CmdClientConfig, error) { + fs := flag.NewFlagSet("client", flag.ContinueOnError) + // Config file + configFile := fs.String("config", "", "Configuration file path") + // Listen + listen := fs.String("listen", "", "TCP listen address") + // Server + server := fs.String("server", "", "Server address") + // Name + name := fs.String("name", "", "Client name presented to the server") + // Insecure + var insecure optionalBoolFlag + fs.Var(&insecure, "insecure", "Ignore TLS certificate errors") + // Custom CA + customCAFile := fs.String("ca", "", "Specify a trusted CA file") + // Up Mbps + upMbps := fs.Int("up-mbps", 0, "Upload speed in Mbps") + // Down Mbps + downMbps := fs.Int("down-mbps", 0, "Download speed in Mbps") + // Receive window conn + recvWindowConn := fs.Uint64("recv-window-conn", 0, "Max receive window size per connection") + // Receive window + recvWindow := fs.Uint64("recv-window", 0, "Max receive window size") + // Parse + if err := fs.Parse(args); err != nil { + os.Exit(1) + } + // Put together the config + var config CmdClientConfig + // Load from file first + if len(*configFile) > 0 { + cb, err := ioutil.ReadFile(*configFile) + if err != nil { + return CmdClientConfig{}, err + } + if err := json.Unmarshal(cb, &config); err != nil { + return CmdClientConfig{}, err + } + } + // Then CLI options can override config + if len(*listen) > 0 { + config.ListenAddr = *listen + } + if len(*server) > 0 { + config.ServerAddr = *server + } + if len(*name) > 0 { + config.Name = *name + } + if insecure.Exists { + config.Insecure = insecure.Value + } + if len(*customCAFile) > 0 { + config.CustomCAFile = *customCAFile + } + if *upMbps != 0 { + config.UpMbps = *upMbps + } + if *downMbps != 0 { + config.DownMbps = *downMbps + } + if *recvWindowConn != 0 { + config.ReceiveWindowConn = *recvWindowConn + } + if *recvWindow != 0 { + config.ReceiveWindow = *recvWindow + } + return config, nil +} + +func client(args []string) { + config, err := loadCmdClientConfig(args) + if err != nil { + log.Fatalln("Unable to load configuration:", err.Error()) + } + if err := config.Check(); err != nil { + log.Fatalln("Configuration error:", err.Error()) + } + if len(config.Name) == 0 { + usr, err := user.Current() + if err == nil { + config.Name = usr.Name + } + } + fmt.Printf("Configuration loaded: %+v\n", config) + + tlsConfig := &tls.Config{ + NextProtos: []string{forwarder.TLSAppProtocol}, + MinVersion: tls.VersionTLS13, + } + + // Load CA + if len(config.CustomCAFile) > 0 { + bs, err := ioutil.ReadFile(config.CustomCAFile) + if err != nil { + log.Fatalln("Unable to load CA file:", err) + } + cp := x509.NewCertPool() + if !cp.AppendCertsFromPEM(bs) { + log.Fatalln("Unable to parse CA file", config.CustomCAFile) + } + tlsConfig.RootCAs = cp + } + + logChan := make(chan string, 4) + + go func() { + _, err = forwarder.NewClient(config.ListenAddr, config.ServerAddr, forwarder.ClientConfig{ + Name: config.Name, + TLSConfig: tlsConfig, + Speed: &forwarder.Speed{ + SendBPS: uint64(config.UpMbps) * mbpsToBps, + ReceiveBPS: uint64(config.DownMbps) * mbpsToBps, + }, + MaxReceiveWindowPerConnection: config.ReceiveWindowConn, + MaxReceiveWindow: config.ReceiveWindow, + CongestionFactory: func(refBPS uint64) congestion.SendAlgorithmWithDebugInfos { + return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS)) + }, + }, forwarder.ClientCallbacks{ + ServerConnectedCallback: func(addr net.Addr, banner string, cSend uint64, cRecv uint64) { + logChan <- fmt.Sprintf("Connected to server %s, negotiated speed in Mbps: Up %d / Down %d", + addr.String(), cSend/mbpsToBps, cRecv/mbpsToBps) + logChan <- fmt.Sprintf("Server banner: [%s]", banner) + }, + ServerErrorCallback: func(err error) { + logChan <- fmt.Sprintf("Error connecting to the server: %s", err.Error()) + }, + NewTCPConnectionCallback: func(addr net.Addr) { + logChan <- fmt.Sprintf("New connection: %s", addr.String()) + }, + TCPConnectionClosedCallback: func(addr net.Addr, err error) { + logChan <- fmt.Sprintf("Connection %s closed: %s", addr.String(), err.Error()) + }, + }) + if err != nil { + log.Fatalln("Client startup failure:", err) + } else { + log.Println("The client is now up and running :)") + } + }() + + for { + logStr := <-logChan + if len(logStr) == 0 { + break + } + log.Println(logStr) + } + +} diff --git a/cmd/forwarder/config.go b/cmd/forwarder/config.go new file mode 100644 index 0000000..e4193a1 --- /dev/null +++ b/cmd/forwarder/config.go @@ -0,0 +1,76 @@ +package main + +import ( + "errors" + "fmt" +) + +type CmdClientConfig struct { + ListenAddr string `json:"listen"` + ServerAddr string `json:"server"` + Name string `json:"name"` + Insecure bool `json:"insecure"` + CustomCAFile string `json:"ca"` + UpMbps int `json:"up_mbps"` + DownMbps int `json:"down_mbps"` + ReceiveWindowConn uint64 `json:"recv_window_conn"` + ReceiveWindow uint64 `json:"recv_window"` +} + +func (c *CmdClientConfig) Check() error { + if len(c.ListenAddr) == 0 { + return errors.New("no listen address") + } + if len(c.ServerAddr) == 0 { + return errors.New("no server address") + } + if c.UpMbps <= 0 || c.DownMbps <= 0 { + return errors.New("invalid speed") + } + if (c.ReceiveWindowConn != 0 && c.ReceiveWindowConn < 65536) || + (c.ReceiveWindow != 0 && c.ReceiveWindow < 65536) { + return errors.New("invalid receive window size") + } + return nil +} + +type ForwardEntry struct { + ListenAddr string `json:"listen"` + RemoteAddr string `json:"remote"` +} + +func (e *ForwardEntry) String() string { + return fmt.Sprintf("%s <-> %s", e.ListenAddr, e.RemoteAddr) +} + +type CmdServerConfig struct { + Entries []ForwardEntry `json:"entries"` + Banner string `json:"banner"` + CertFile string `json:"cert"` + KeyFile string `json:"key"` + UpMbps int `json:"up_mbps"` + DownMbps int `json:"down_mbps"` + ReceiveWindowConn uint64 `json:"recv_window_conn"` + ReceiveWindowClient uint64 `json:"recv_window_client"` + MaxConnClient int `json:"max_conn_client"` +} + +func (c *CmdServerConfig) Check() error { + if len(c.Entries) == 0 { + return errors.New("no entries") + } + if len(c.CertFile) == 0 || len(c.KeyFile) == 0 { + return errors.New("TLS cert or key not provided") + } + if c.UpMbps < 0 || c.DownMbps < 0 { + return errors.New("invalid speed") + } + if (c.ReceiveWindowConn != 0 && c.ReceiveWindowConn < 65536) || + (c.ReceiveWindowClient != 0 && c.ReceiveWindowClient < 65536) { + return errors.New("invalid receive window size") + } + if c.MaxConnClient < 0 { + return errors.New("invalid max connections per client") + } + return nil +} diff --git a/cmd/forwarder/flags.go b/cmd/forwarder/flags.go new file mode 100644 index 0000000..41529b9 --- /dev/null +++ b/cmd/forwarder/flags.go @@ -0,0 +1,40 @@ +package main + +import ( + "strconv" + "strings" +) + +type optionalBoolFlag struct { + Exists bool + Value bool +} + +func (flag *optionalBoolFlag) String() string { + return strconv.FormatBool(flag.Value) +} + +func (flag *optionalBoolFlag) Set(s string) error { + v, err := strconv.ParseBool(s) + if err != nil { + return err + } + flag.Exists = true + flag.Value = v + return nil +} + +func (o *optionalBoolFlag) IsBoolFlag() bool { + return true +} + +type stringSliceFlag []string + +func (flag *stringSliceFlag) String() string { + return strings.Join(*flag, ";") +} + +func (flag *stringSliceFlag) Set(s string) error { + *flag = append(*flag, s) + return nil +} diff --git a/cmd/forwarder/main.go b/cmd/forwarder/main.go new file mode 100644 index 0000000..65515c7 --- /dev/null +++ b/cmd/forwarder/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "os" + "strings" +) + +func main() { + if len(os.Args) < 2 { + fmt.Println() + fmt.Printf("Usage: %s MODE [OPTIONS]\n\n"+ + "Modes: server / client\n"+ + "Use -h to see the available options for a mode.\n\n", os.Args[0]) + return + } + mode := strings.ToLower(strings.TrimSpace(os.Args[1])) + switch mode { + case "server", "s": + server(os.Args[2:]) + case "client", "c": + client(os.Args[2:]) + default: + fmt.Println("Invalid mode:", mode) + } +} diff --git a/cmd/forwarder/server.go b/cmd/forwarder/server.go new file mode 100644 index 0000000..25a94da --- /dev/null +++ b/cmd/forwarder/server.go @@ -0,0 +1,204 @@ +package main + +import ( + "crypto/tls" + "encoding/json" + "flag" + "fmt" + "github.com/lucas-clemente/quic-go/congestion" + hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion" + "github.com/tobyxdd/hysteria/pkg/forwarder" + "io/ioutil" + "log" + "net" + "os" + "strings" +) + +const mbpsToBps = 125000 + +func loadCmdServerConfig(args []string) (CmdServerConfig, error) { + fs := flag.NewFlagSet("server", flag.ContinueOnError) + // Config file + configFile := fs.String("config", "", "Configuration file path") + // Entries + var entries stringSliceFlag + fs.Var(&entries, "entry", "Add a forwarding entry. Separate the listen address and the remote address with a comma. You can add this option multiple times. Example: localhost:444,google.com:443") + // Banner + banner := fs.String("banner", "", "A banner to present to clients") + // Cert file + certFile := fs.String("cert", "", "TLS certificate file") + // Key file + keyFile := fs.String("key", "", "TLS key file") + // Up Mbps + upMbps := fs.Int("up-mbps", 0, "Max upload speed per client in Mbps") + // Down Mbps + downMbps := fs.Int("down-mbps", 0, "Max download speed per client in Mbps") + // Receive window conn + recvWindowConn := fs.Uint64("recv-window-conn", 0, "Max receive window size per connection") + // Receive window client + recvWindowClient := fs.Uint64("recv-window-client", 0, "Max receive window size per client") + // Max conn client + maxConnClient := fs.Int("max-conn-client", 0, "Max simultaneous connections allowed per client") + // Parse + if err := fs.Parse(args); err != nil { + os.Exit(1) + } + // Put together the config + var config CmdServerConfig + // Load from file first + if len(*configFile) > 0 { + cb, err := ioutil.ReadFile(*configFile) + if err != nil { + return CmdServerConfig{}, err + } + if err := json.Unmarshal(cb, &config); err != nil { + return CmdServerConfig{}, err + } + } + // Then CLI options can override config + if len(entries) > 0 { + fe, err := flagToEntries(entries) + if err != nil { + return CmdServerConfig{}, err + } + config.Entries = append(config.Entries, fe...) + } + if len(*banner) > 0 { + config.Banner = *banner + } + if len(*certFile) > 0 { + config.CertFile = *certFile + } + if len(*keyFile) > 0 { + config.KeyFile = *keyFile + } + if *upMbps != 0 { + config.UpMbps = *upMbps + } + if *downMbps != 0 { + config.DownMbps = *downMbps + } + if *recvWindowConn != 0 { + config.ReceiveWindowConn = *recvWindowConn + } + if *recvWindowClient != 0 { + config.ReceiveWindowClient = *recvWindowClient + } + if *maxConnClient != 0 { + config.MaxConnClient = *maxConnClient + } + return config, nil +} + +func flagToEntries(f stringSliceFlag) ([]ForwardEntry, error) { + out := make([]ForwardEntry, len(f)) + for i, entry := range f { + es := strings.Split(entry, ",") + if len(es) != 2 { + return nil, fmt.Errorf("incorrect entry syntax: %s", entry) + } + out[i] = ForwardEntry{ + ListenAddr: es[0], + RemoteAddr: es[1], + } + } + return out, nil +} + +func server(args []string) { + config, err := loadCmdServerConfig(args) + if err != nil { + log.Fatalln("Unable to load configuration:", err.Error()) + } + if err := config.Check(); err != nil { + log.Fatalln("Configuration error:", err.Error()) + } + fmt.Printf("Configuration loaded: %+v\n", config) + // Load cert + cert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile) + if err != nil { + log.Fatalln("Unable to load the certificate:", err) + } + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + NextProtos: []string{forwarder.TLSAppProtocol}, + MinVersion: tls.VersionTLS13, + } + + logChan := make(chan string, 4) + + go func() { + server := forwarder.NewServer(forwarder.ServerConfig{ + BannerMessage: config.Banner, + TLSConfig: tlsConfig, + MaxSpeedPerClient: &forwarder.Speed{ + SendBPS: uint64(config.UpMbps) * mbpsToBps, + ReceiveBPS: uint64(config.DownMbps) * mbpsToBps, + }, + MaxReceiveWindowPerConnection: config.ReceiveWindowConn, + MaxReceiveWindowPerClient: config.ReceiveWindowClient, + MaxConnectionPerClient: config.MaxConnClient, + CongestionFactory: func(refBPS uint64) congestion.SendAlgorithmWithDebugInfos { + return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS)) + }, + }, forwarder.ServerCallbacks{ + ClientConnectedCallback: func(listenAddr string, clientAddr net.Addr, name string, sSend uint64, sRecv uint64) { + if len(name) > 0 { + logChan <- fmt.Sprintf("[%s] Client %s (%s) connected, negotiated speed in Mbps: Up %d / Down %d", + listenAddr, clientAddr.String(), name, sSend/mbpsToBps, sRecv/mbpsToBps) + } else { + logChan <- fmt.Sprintf("[%s] Client %s connected, negotiated speed in Mbps: Up %d / Down %d", + listenAddr, clientAddr.String(), sSend/mbpsToBps, sRecv/mbpsToBps) + } + }, + ClientDisconnectedCallback: func(listenAddr string, clientAddr net.Addr, name string, err error) { + if len(name) > 0 { + logChan <- fmt.Sprintf("[%s] Client %s (%s) disconnected: %s", + listenAddr, clientAddr.String(), name, err.Error()) + } else { + logChan <- fmt.Sprintf("[%s] Client %s disconnected: %s", + listenAddr, clientAddr.String(), err.Error()) + } + }, + ClientNewStreamCallback: func(listenAddr string, clientAddr net.Addr, name string, id int) { + if len(name) > 0 { + logChan <- fmt.Sprintf("[%s] Client %s (%s) opened stream ID %d", + listenAddr, clientAddr.String(), name, id) + } else { + logChan <- fmt.Sprintf("[%s] Client %s opened stream ID %d", + listenAddr, clientAddr.String(), id) + } + }, + ClientStreamClosedCallback: func(listenAddr string, clientAddr net.Addr, name string, id int, err error) { + if len(name) > 0 { + logChan <- fmt.Sprintf("[%s] Client %s (%s) closed stream ID %d: %s", + listenAddr, clientAddr.String(), name, id, err.Error()) + } else { + logChan <- fmt.Sprintf("[%s] Client %s closed stream ID %d: %s", + listenAddr, clientAddr.String(), id, err.Error()) + } + }, + TCPErrorCallback: func(listenAddr string, remoteAddr string, err error) { + logChan <- fmt.Sprintf("[%s] TCP error when connecting to %s: %s", + listenAddr, remoteAddr, err.Error()) + }, + }) + for _, entry := range config.Entries { + log.Println("Starting", entry.String(), "...") + if err := server.Add(entry.ListenAddr, entry.RemoteAddr); err != nil { + log.Fatalln(err) + } + } + log.Println("The server is now up and running :)") + }() + + for { + logStr := <-logChan + if len(logStr) == 0 { + break + } + log.Println(logStr) + } + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ca0d4ef --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/tobyxdd/hysteria + +go 1.14 + +require github.com/golang/protobuf v1.3.1 + +require github.com/lucas-clemente/quic-go v0.15.2 + +replace github.com/lucas-clemente/quic-go => github.com/tobyxdd/quic-go v0.1.1-tquic-1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9a5c0c4 --- /dev/null +++ b/go.sum @@ -0,0 +1,211 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75 h1:3ILjVyslFbc4jl1w5TWuvvslFD/nDfR2H8tVaMVLrEY= +github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI= +github.com/marten-seemann/qtls v0.8.0 h1:aj+MPLibzKByw8CmG0WvWgbtBkctYPAXeB11cQJC8mo= +github.com/marten-seemann/qtls v0.8.0/go.mod h1:Lao6jDqlCfxyLKYFmZXGm2LSHBgVn+P+ROOex6YkT+k= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= +github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tobyxdd/quic-go v0.1.1-tquic-1 h1:KDhDFNe+IlI+ZOvEG6wA5JcNq8OUO029OYi8bq7XnSU= +github.com/tobyxdd/quic-go v0.1.1-tquic-1/go.mod h1:qxmO5Y4ZMhdNkunGfxuZnZXnJwYpW9vjQkyrZ7BsgUI= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/internal/forwarder/client.go b/internal/forwarder/client.go new file mode 100644 index 0000000..7813023 --- /dev/null +++ b/internal/forwarder/client.go @@ -0,0 +1,200 @@ +package forwarder + +import ( + "context" + "crypto/tls" + "errors" + "github.com/lucas-clemente/quic-go" + "github.com/tobyxdd/hysteria/internal/utils" + "net" + "sync" + "sync/atomic" +) + +type QUICClient struct { + inboundBytes, outboundBytes uint64 // atomic + + reconnectMutex sync.Mutex + quicSession quic.Session + listener net.Listener + remoteAddr string + name string + tlsConfig *tls.Config + sendBPS, recvBPS uint64 + recvWindowConn, recvWindow uint64 + closed bool + + newCongestion CongestionFactory + onServerConnected ServerConnectedCallback + onServerError ServerErrorCallback + onNewTCPConnection NewTCPConnectionCallback + onTCPConnectionClosed TCPConnectionClosedCallback +} + +func NewQUICClient(addr string, remoteAddr string, name string, tlsConfig *tls.Config, + sendBPS uint64, recvBPS uint64, recvWindowConn uint64, recvWindow uint64, + newCongestion CongestionFactory, + onServerConnected ServerConnectedCallback, + onServerError ServerErrorCallback, + onNewTCPConnection NewTCPConnectionCallback, + onTCPConnectionClosed TCPConnectionClosedCallback) (*QUICClient, error) { + // Local TCP listener + listener, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + c := &QUICClient{ + listener: listener, + remoteAddr: remoteAddr, + name: name, + tlsConfig: tlsConfig, + sendBPS: sendBPS, + recvBPS: recvBPS, + recvWindowConn: recvWindowConn, + recvWindow: recvWindow, + newCongestion: newCongestion, + onServerConnected: onServerConnected, + onServerError: onServerError, + onNewTCPConnection: onNewTCPConnection, + onTCPConnectionClosed: onTCPConnectionClosed, + } + if err := c.connectToServer(); err != nil { + _ = c.listener.Close() + return nil, err + } + go c.acceptLoop() + return c, nil +} + +func (c *QUICClient) Close() error { + err1 := c.listener.Close() + c.reconnectMutex.Lock() + err2 := c.quicSession.CloseWithError(closeErrorCodeGeneric, "generic") + c.closed = true + c.reconnectMutex.Unlock() + if err1 != nil { + return err1 + } + return err2 +} + +func (c *QUICClient) Stats() (string, uint64, uint64) { + return c.remoteAddr, atomic.LoadUint64(&c.inboundBytes), atomic.LoadUint64(&c.outboundBytes) +} + +func (c *QUICClient) acceptLoop() { + for { + conn, err := c.listener.Accept() + if err != nil { + break + } + go c.handleConn(conn) + } +} + +func (c *QUICClient) connectToServer() error { + qs, err := quic.DialAddr(c.remoteAddr, c.tlsConfig, &quic.Config{ + MaxReceiveStreamFlowControlWindow: c.recvWindowConn, + MaxReceiveConnectionFlowControlWindow: c.recvWindow, + KeepAlive: true, + }) + if err != nil { + c.onServerError(err) + return err + } + // Control stream + ctx, ctxCancel := context.WithTimeout(context.Background(), controlStreamTimeout) + ctlStream, err := qs.OpenStreamSync(ctx) + ctxCancel() + if err != nil { + _ = qs.CloseWithError(closeErrorCodeProtocolFailure, "control stream error") + c.onServerError(err) + return err + } + banner, cSendBPS, cRecvBPS, err := handleControlStream(qs, ctlStream, c.name, c.sendBPS, c.recvBPS, c.newCongestion) + if err != nil { + _ = qs.CloseWithError(closeErrorCodeProtocolFailure, "control stream handling error") + c.onServerError(err) + return err + } + // All good + c.quicSession = qs + c.onServerConnected(qs.RemoteAddr(), banner, cSendBPS, cRecvBPS) + return nil +} + +func (c *QUICClient) openStreamWithReconnect() (quic.Stream, error) { + c.reconnectMutex.Lock() + defer c.reconnectMutex.Unlock() + if c.closed { + return nil, errors.New("client closed") + } + stream, err := c.quicSession.OpenStream() + if err == nil { + // All good + return stream, nil + } + // Something is wrong + c.onServerError(err) + if nErr, ok := err.(net.Error); ok && nErr.Temporary() { + // Temporary error, just return + return nil, err + } + // Permanent error, need to reconnect + if err := c.connectToServer(); err != nil { + // Still error, oops + return nil, err + } + // We are not going to try again even if it still fails the second time + stream, err = c.quicSession.OpenStream() + if err != nil { + c.onServerError(err) + } + return stream, err +} + +// Negotiate speed, return banner, send & receive speed +func handleControlStream(qs quic.Session, stream quic.Stream, name string, sendBPS uint64, recvBPS uint64, + newCongestion CongestionFactory) (string, uint64, uint64, error) { + err := writeClientSpeedRequest(stream, &ClientSpeedRequest{ + Name: name, + Speed: &Speed{ + SendBps: sendBPS, + ReceiveBps: recvBPS, + }, + }) + if err != nil { + return "", 0, 0, err + } + // Response + resp, err := readServerSpeedResponse(stream) + if err != nil { + return "", 0, 0, err + } + // Set the congestion accordingly + if newCongestion != nil { + qs.SetCongestion(newCongestion(resp.Speed.ReceiveBps)) + } + return resp.Banner, resp.Speed.ReceiveBps, resp.Speed.SendBps, nil +} + +func (c *QUICClient) handleConn(conn net.Conn) { + c.onNewTCPConnection(conn.RemoteAddr()) + defer conn.Close() + stream, err := c.openStreamWithReconnect() + if err != nil { + c.onTCPConnectionClosed(conn.RemoteAddr(), err) + return + } + defer stream.Close() + // From TCP to QUIC + go func() { + _ = utils.Pipe(conn, stream, &c.outboundBytes) + _ = conn.Close() + _ = stream.Close() + }() + // From QUIC to TCP + err = utils.Pipe(stream, conn, &c.inboundBytes) + // Closed + c.onTCPConnectionClosed(conn.RemoteAddr(), err) +} diff --git a/internal/forwarder/control.go b/internal/forwarder/control.go new file mode 100644 index 0000000..398ef59 --- /dev/null +++ b/internal/forwarder/control.go @@ -0,0 +1,67 @@ +package forwarder + +import ( + "encoding/binary" + "github.com/golang/protobuf/proto" + "io" +) + +const ( + closeErrorCodeGeneric = 0 + closeErrorCodeProtocolFailure = 1 +) + +func readDataBlock(r io.Reader) ([]byte, error) { + var sz uint32 + if err := binary.Read(r, controlProtocolEndian, &sz); err != nil { + return nil, err + } + buf := make([]byte, sz) + _, err := io.ReadFull(r, buf) + return buf, err +} + +func writeDataBlock(w io.Writer, data []byte) error { + sz := uint32(len(data)) + if err := binary.Write(w, controlProtocolEndian, &sz); err != nil { + return err + } + _, err := w.Write(data) + return err +} + +func readClientSpeedRequest(r io.Reader) (*ClientSpeedRequest, error) { + bs, err := readDataBlock(r) + if err != nil { + return nil, err + } + var req ClientSpeedRequest + err = proto.Unmarshal(bs, &req) + return &req, err +} + +func writeClientSpeedRequest(w io.Writer, req *ClientSpeedRequest) error { + bs, err := proto.Marshal(req) + if err != nil { + return err + } + return writeDataBlock(w, bs) +} + +func readServerSpeedResponse(r io.Reader) (*ServerSpeedResponse, error) { + bs, err := readDataBlock(r) + if err != nil { + return nil, err + } + var resp ServerSpeedResponse + err = proto.Unmarshal(bs, &resp) + return &resp, err +} + +func writeServerSpeedResponse(w io.Writer, resp *ServerSpeedResponse) error { + bs, err := proto.Marshal(resp) + if err != nil { + return err + } + return writeDataBlock(w, bs) +} diff --git a/internal/forwarder/control.pb.go b/internal/forwarder/control.pb.go new file mode 100644 index 0000000..21b5df9 --- /dev/null +++ b/internal/forwarder/control.pb.go @@ -0,0 +1,206 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: control.proto + +package forwarder + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type Speed struct { + SendBps uint64 `protobuf:"varint,1,opt,name=send_bps,json=sendBps,proto3" json:"send_bps,omitempty"` + ReceiveBps uint64 `protobuf:"varint,2,opt,name=receive_bps,json=receiveBps,proto3" json:"receive_bps,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Speed) Reset() { *m = Speed{} } +func (m *Speed) String() string { return proto.CompactTextString(m) } +func (*Speed) ProtoMessage() {} +func (*Speed) Descriptor() ([]byte, []int) { + return fileDescriptor_0c5120591600887d, []int{0} +} + +func (m *Speed) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Speed.Unmarshal(m, b) +} +func (m *Speed) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Speed.Marshal(b, m, deterministic) +} +func (m *Speed) XXX_Merge(src proto.Message) { + xxx_messageInfo_Speed.Merge(m, src) +} +func (m *Speed) XXX_Size() int { + return xxx_messageInfo_Speed.Size(m) +} +func (m *Speed) XXX_DiscardUnknown() { + xxx_messageInfo_Speed.DiscardUnknown(m) +} + +var xxx_messageInfo_Speed proto.InternalMessageInfo + +func (m *Speed) GetSendBps() uint64 { + if m != nil { + return m.SendBps + } + return 0 +} + +func (m *Speed) GetReceiveBps() uint64 { + if m != nil { + return m.ReceiveBps + } + return 0 +} + +type ClientSpeedRequest struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Speed *Speed `protobuf:"bytes,2,opt,name=speed,proto3" json:"speed,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ClientSpeedRequest) Reset() { *m = ClientSpeedRequest{} } +func (m *ClientSpeedRequest) String() string { return proto.CompactTextString(m) } +func (*ClientSpeedRequest) ProtoMessage() {} +func (*ClientSpeedRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_0c5120591600887d, []int{1} +} + +func (m *ClientSpeedRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ClientSpeedRequest.Unmarshal(m, b) +} +func (m *ClientSpeedRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ClientSpeedRequest.Marshal(b, m, deterministic) +} +func (m *ClientSpeedRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ClientSpeedRequest.Merge(m, src) +} +func (m *ClientSpeedRequest) XXX_Size() int { + return xxx_messageInfo_ClientSpeedRequest.Size(m) +} +func (m *ClientSpeedRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ClientSpeedRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ClientSpeedRequest proto.InternalMessageInfo + +func (m *ClientSpeedRequest) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *ClientSpeedRequest) GetSpeed() *Speed { + if m != nil { + return m.Speed + } + return nil +} + +type ServerSpeedResponse struct { + Banner string `protobuf:"bytes,1,opt,name=banner,proto3" json:"banner,omitempty"` + Limited bool `protobuf:"varint,2,opt,name=limited,proto3" json:"limited,omitempty"` + Limit *Speed `protobuf:"bytes,3,opt,name=limit,proto3" json:"limit,omitempty"` + Speed *Speed `protobuf:"bytes,4,opt,name=speed,proto3" json:"speed,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ServerSpeedResponse) Reset() { *m = ServerSpeedResponse{} } +func (m *ServerSpeedResponse) String() string { return proto.CompactTextString(m) } +func (*ServerSpeedResponse) ProtoMessage() {} +func (*ServerSpeedResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_0c5120591600887d, []int{2} +} + +func (m *ServerSpeedResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ServerSpeedResponse.Unmarshal(m, b) +} +func (m *ServerSpeedResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ServerSpeedResponse.Marshal(b, m, deterministic) +} +func (m *ServerSpeedResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ServerSpeedResponse.Merge(m, src) +} +func (m *ServerSpeedResponse) XXX_Size() int { + return xxx_messageInfo_ServerSpeedResponse.Size(m) +} +func (m *ServerSpeedResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ServerSpeedResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ServerSpeedResponse proto.InternalMessageInfo + +func (m *ServerSpeedResponse) GetBanner() string { + if m != nil { + return m.Banner + } + return "" +} + +func (m *ServerSpeedResponse) GetLimited() bool { + if m != nil { + return m.Limited + } + return false +} + +func (m *ServerSpeedResponse) GetLimit() *Speed { + if m != nil { + return m.Limit + } + return nil +} + +func (m *ServerSpeedResponse) GetSpeed() *Speed { + if m != nil { + return m.Speed + } + return nil +} + +func init() { + proto.RegisterType((*Speed)(nil), "forwarder.Speed") + proto.RegisterType((*ClientSpeedRequest)(nil), "forwarder.ClientSpeedRequest") + proto.RegisterType((*ServerSpeedResponse)(nil), "forwarder.ServerSpeedResponse") +} + +func init() { + proto.RegisterFile("control.proto", fileDescriptor_0c5120591600887d) +} + +var fileDescriptor_0c5120591600887d = []byte{ + // 220 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x90, 0x4d, 0x4a, 0xc6, 0x30, + 0x10, 0x86, 0xa9, 0xf6, 0xfb, 0x9b, 0x0f, 0x41, 0x46, 0x90, 0xba, 0x52, 0xba, 0x10, 0x57, 0x5d, + 0xe8, 0x0d, 0xbe, 0x5e, 0x40, 0xd2, 0x03, 0x48, 0x7f, 0x46, 0x08, 0xb4, 0x49, 0x9c, 0x89, 0xf5, + 0x28, 0x5e, 0x57, 0x3a, 0x8d, 0xba, 0xd2, 0xdd, 0xbc, 0x79, 0x92, 0x67, 0x5e, 0x02, 0x17, 0xbd, + 0x77, 0x91, 0xfd, 0x58, 0x05, 0xf6, 0xd1, 0xe3, 0xe1, 0xd5, 0xf3, 0x47, 0xcb, 0x03, 0x71, 0x59, + 0xc3, 0xa6, 0x09, 0x44, 0x03, 0xde, 0xc0, 0x5e, 0xc8, 0x0d, 0x2f, 0x5d, 0x90, 0x22, 0xbb, 0xcb, + 0x1e, 0x72, 0xb3, 0x5b, 0xf2, 0x29, 0x08, 0xde, 0xc2, 0x91, 0xa9, 0x27, 0x3b, 0x93, 0xd2, 0x33, + 0xa5, 0x90, 0x8e, 0x4e, 0x41, 0xca, 0x67, 0xc0, 0x7a, 0xb4, 0xe4, 0xa2, 0xaa, 0x0c, 0xbd, 0xbd, + 0x93, 0x44, 0x44, 0xc8, 0x5d, 0x3b, 0x91, 0xda, 0x0e, 0x46, 0x67, 0xbc, 0x87, 0x8d, 0x2c, 0x77, + 0x54, 0x72, 0x7c, 0xbc, 0xac, 0x7e, 0x9a, 0x54, 0xeb, 0xdb, 0x15, 0x97, 0x9f, 0x19, 0x5c, 0x35, + 0xc4, 0x33, 0x71, 0x52, 0x4a, 0xf0, 0x4e, 0x08, 0xaf, 0x61, 0xdb, 0xb5, 0xce, 0x11, 0x27, 0x6b, + 0x4a, 0x58, 0xc0, 0x6e, 0xb4, 0x93, 0x8d, 0xc9, 0xbc, 0x37, 0xdf, 0x71, 0xd9, 0xa8, 0x63, 0x71, + 0xfe, 0xd7, 0x46, 0xc5, 0xbf, 0xcd, 0xf2, 0x7f, 0x9b, 0x75, 0x5b, 0xfd, 0xc2, 0xa7, 0xaf, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xb2, 0x10, 0x5a, 0xf2, 0x53, 0x01, 0x00, 0x00, +} diff --git a/internal/forwarder/control.proto b/internal/forwarder/control.proto new file mode 100644 index 0000000..f6c1b9a --- /dev/null +++ b/internal/forwarder/control.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; +package forwarder; + +message Speed { + uint64 send_bps = 1; + uint64 receive_bps = 2; +} + +message ClientSpeedRequest { + string name = 1; + Speed speed = 2; +} + +message ServerSpeedResponse { + string banner = 1; + bool limited = 2; + Speed limit = 3; + Speed speed = 4; +} \ No newline at end of file diff --git a/internal/forwarder/params.go b/internal/forwarder/params.go new file mode 100644 index 0000000..0b240a3 --- /dev/null +++ b/internal/forwarder/params.go @@ -0,0 +1,10 @@ +package forwarder + +import ( + "encoding/binary" + "time" +) + +const controlStreamTimeout = 10 * time.Second + +var controlProtocolEndian = binary.BigEndian diff --git a/internal/forwarder/protogen.go b/internal/forwarder/protogen.go new file mode 100644 index 0000000..aa1f7f3 --- /dev/null +++ b/internal/forwarder/protogen.go @@ -0,0 +1,3 @@ +package forwarder + +//go:generate protoc --go_out=. control.proto diff --git a/internal/forwarder/server.go b/internal/forwarder/server.go new file mode 100644 index 0000000..9d7049c --- /dev/null +++ b/internal/forwarder/server.go @@ -0,0 +1,173 @@ +package forwarder + +import ( + "context" + "crypto/tls" + "errors" + "github.com/lucas-clemente/quic-go" + "github.com/tobyxdd/hysteria/internal/utils" + "net" + "sync/atomic" +) + +type QUICServer struct { + inboundBytes, outboundBytes uint64 // atomic + + listener quic.Listener + remoteAddr string + banner string + sendBPS, recvBPS uint64 + + newCongestion CongestionFactory + onClientConnected ClientConnectedCallback + onClientDisconnected ClientDisconnectedCallback + onClientNewStream ClientNewStreamCallback + onClientStreamClosed ClientStreamClosedCallback + onTCPError TCPErrorCallback +} + +func NewQUICServer(addr string, remoteAddr string, banner string, tlsConfig *tls.Config, + sendBPS uint64, recvBPS uint64, recvWindowConn uint64, recvWindowClients uint64, + clientMaxConn int, newCongestion CongestionFactory, + onClientConnected ClientConnectedCallback, + onClientDisconnected ClientDisconnectedCallback, + onClientNewStream ClientNewStreamCallback, + onClientStreamClosed ClientStreamClosedCallback, + onTCPError TCPErrorCallback) (*QUICServer, error) { + listener, err := quic.ListenAddr(addr, tlsConfig, &quic.Config{ + MaxReceiveStreamFlowControlWindow: recvWindowConn, + MaxReceiveConnectionFlowControlWindow: recvWindowClients, + MaxIncomingStreams: clientMaxConn, + KeepAlive: true, + }) + if err != nil { + return nil, err + } + s := &QUICServer{ + listener: listener, + remoteAddr: remoteAddr, + banner: banner, + sendBPS: sendBPS, + recvBPS: recvBPS, + newCongestion: newCongestion, + onClientConnected: onClientConnected, + onClientDisconnected: onClientDisconnected, + onClientNewStream: onClientNewStream, + onClientStreamClosed: onClientStreamClosed, + onTCPError: onTCPError, + } + go s.acceptLoop() + return s, nil +} + +func (s *QUICServer) Close() error { + return s.listener.Close() +} + +func (s *QUICServer) Stats() (string, uint64, uint64) { + return s.remoteAddr, atomic.LoadUint64(&s.inboundBytes), atomic.LoadUint64(&s.outboundBytes) +} + +func (s *QUICServer) acceptLoop() { + for { + cs, err := s.listener.Accept(context.Background()) + if err != nil { + break + } + go s.handleClient(cs) + } +} + +func (s *QUICServer) handleClient(cs quic.Session) { + // Expect the client to create a control stream and send its own information + ctx, ctxCancel := context.WithTimeout(context.Background(), controlStreamTimeout) + ctlStream, err := cs.AcceptStream(ctx) + ctxCancel() + if err != nil { + _ = cs.CloseWithError(closeErrorCodeProtocolFailure, "control stream error") + return + } + name, sSend, sRecv, err := s.handleControlStream(cs, ctlStream) + if err != nil { + _ = cs.CloseWithError(closeErrorCodeProtocolFailure, "control stream handling error") + return + } + // Only after a successful exchange of information do we consider this a valid client + s.onClientConnected(cs.RemoteAddr(), name, sSend, sRecv) + // Start accepting streams to be forwarded + var closeErr error + for { + stream, err := cs.AcceptStream(context.Background()) + if err != nil { + closeErr = err + break + } + go s.handleStream(cs.RemoteAddr(), name, stream) + } + s.onClientDisconnected(cs.RemoteAddr(), name, closeErr) + _ = cs.CloseWithError(closeErrorCodeGeneric, "generic") +} + +// Negotiate speed & return client name +func (s *QUICServer) handleControlStream(cs quic.Session, stream quic.Stream) (string, uint64, uint64, error) { + req, err := readClientSpeedRequest(stream) + if err != nil { + return "", 0, 0, err + } + if req.Speed == nil || req.Speed.SendBps == 0 || req.Speed.ReceiveBps == 0 { + return "", 0, 0, errors.New("incorrect speed information provided by the client") + } + limited := false + serverSendBPS, serverReceiveBPS := req.Speed.ReceiveBps, req.Speed.SendBps + if s.sendBPS > 0 && serverSendBPS > s.sendBPS { + limited = true + serverSendBPS = s.sendBPS + } + if s.recvBPS > 0 && serverReceiveBPS > s.recvBPS { + limited = true + serverReceiveBPS = s.recvBPS + } + // Response + err = writeServerSpeedResponse(stream, &ServerSpeedResponse{ + Banner: s.banner, + Limited: limited, + Limit: &Speed{ + SendBps: s.sendBPS, + ReceiveBps: s.recvBPS, + }, + Speed: &Speed{ + SendBps: serverSendBPS, + ReceiveBps: serverReceiveBPS, + }, + }) + if err != nil { + return "", 0, 0, err + } + // Set the congestion accordingly + if s.newCongestion != nil { + cs.SetCongestion(s.newCongestion(serverSendBPS)) + } + return req.Name, serverSendBPS, serverReceiveBPS, nil +} + +func (s *QUICServer) handleStream(addr net.Addr, name string, stream quic.Stream) { + s.onClientNewStream(addr, name, int(stream.StreamID())) + defer stream.Close() + tcpConn, err := net.Dial("tcp", s.remoteAddr) + if err != nil { + s.onTCPError(s.remoteAddr, err) + s.onClientStreamClosed(addr, name, int(stream.StreamID()), err) + return + } + defer tcpConn.Close() + // From TCP to QUIC + go func() { + _ = utils.Pipe(tcpConn, stream, &s.outboundBytes) + _ = tcpConn.Close() + _ = stream.Close() + }() + // From QUIC to TCP + err = utils.Pipe(stream, tcpConn, &s.inboundBytes) + // Closed + s.onClientStreamClosed(addr, name, int(stream.StreamID()), err) +} diff --git a/internal/forwarder/types.go b/internal/forwarder/types.go new file mode 100644 index 0000000..f8c3156 --- /dev/null +++ b/internal/forwarder/types.go @@ -0,0 +1,21 @@ +package forwarder + +import ( + "github.com/lucas-clemente/quic-go/congestion" + "net" +) + +type CongestionFactory func(refBPS uint64) congestion.SendAlgorithmWithDebugInfos + +// For server +type ClientConnectedCallback func(addr net.Addr, name string, sSend uint64, sRecv uint64) +type ClientDisconnectedCallback func(addr net.Addr, name string, err error) +type ClientNewStreamCallback func(addr net.Addr, name string, id int) +type ClientStreamClosedCallback func(addr net.Addr, name string, id int, err error) +type TCPErrorCallback func(remoteAddr string, err error) + +// For client +type ServerConnectedCallback func(addr net.Addr, banner string, cSend uint64, cRecv uint64) +type ServerErrorCallback func(err error) +type NewTCPConnectionCallback func(addr net.Addr) +type TCPConnectionClosedCallback func(addr net.Addr, err error) diff --git a/internal/utils/pipe.go b/internal/utils/pipe.go new file mode 100644 index 0000000..a0dd250 --- /dev/null +++ b/internal/utils/pipe.go @@ -0,0 +1,25 @@ +package utils + +import ( + "io" + "sync/atomic" +) + +const pipeBufferSize = 16384 + +func Pipe(src, dst io.ReadWriter, atomicCounter *uint64) error { + buf := make([]byte, pipeBufferSize) + for { + rn, err := src.Read(buf) + if rn > 0 { + wn, err := dst.Write(buf[:rn]) + atomic.AddUint64(atomicCounter, uint64(wn)) + if err != nil { + return err + } + } + if err != nil { + return err + } + } +} diff --git a/pkg/congestion/brutal.go b/pkg/congestion/brutal.go new file mode 100644 index 0000000..ae22e35 --- /dev/null +++ b/pkg/congestion/brutal.go @@ -0,0 +1,70 @@ +package congestion + +import ( + "github.com/lucas-clemente/quic-go/congestion" + "time" +) + +// BrutalSender sends packets at a constant rate and does not react to any changes in the network environment, +// hence the name. +type BrutalSender struct { + rttStats *congestion.RTTStats + bps congestion.ByteCount +} + +func NewBrutalSender(bps congestion.ByteCount) *BrutalSender { + return &BrutalSender{ + bps: bps, + } +} + +func (b *BrutalSender) SetRTTStats(rttStats *congestion.RTTStats) { + b.rttStats = rttStats +} + +func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Duration { + return time.Duration(congestion.ByteCount(time.Second) * congestion.MaxPacketSizeIPv4 / (2 * b.bps)) +} + +func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool { + return bytesInFlight < b.GetCongestionWindow() +} + +func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount { + rtt := maxDuration(b.rttStats.LatestRTT(), b.rttStats.SmoothedRTT()) + if rtt <= 0 { + return 10240 + } + return b.bps * congestion.ByteCount(rtt) / congestion.ByteCount(time.Second) +} + +func (b *BrutalSender) OnPacketSent(sentTime time.Time, bytesInFlight congestion.ByteCount, + packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool) { +} + +func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount, + priorInFlight congestion.ByteCount, eventTime time.Time) { +} + +func (b *BrutalSender) OnPacketLost(number congestion.PacketNumber, lostBytes congestion.ByteCount, + priorInFlight congestion.ByteCount) { +} + +func (b *BrutalSender) InSlowStart() bool { + return false +} + +func (b *BrutalSender) InRecovery() bool { + return false +} + +func (b *BrutalSender) MaybeExitSlowStart() {} + +func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {} + +func maxDuration(a, b time.Duration) time.Duration { + if a > b { + return a + } + return b +} diff --git a/pkg/forwarder/client.go b/pkg/forwarder/client.go new file mode 100644 index 0000000..c75e993 --- /dev/null +++ b/pkg/forwarder/client.go @@ -0,0 +1,70 @@ +package forwarder + +import ( + "crypto/tls" + "errors" + "github.com/tobyxdd/hysteria/internal/forwarder" + "net" +) + +type client struct { + qc *forwarder.QUICClient +} + +func NewClient(localAddr string, remoteAddr string, config ClientConfig, callbacks ClientCallbacks) (Client, error) { + // Fix config first + if config.Speed == nil || config.Speed.SendBPS == 0 || config.Speed.ReceiveBPS == 0 { + return nil, errors.New("invalid speed") + } + if config.TLSConfig == nil { + config.TLSConfig = &tls.Config{NextProtos: []string{TLSAppProtocol}} + } + if config.MaxReceiveWindowPerConnection == 0 { + config.MaxReceiveWindowPerConnection = defaultReceiveWindowConn + } + if config.MaxReceiveWindow == 0 { + config.MaxReceiveWindow = defaultReceiveWindow + } + qc, err := forwarder.NewQUICClient(localAddr, remoteAddr, config.Name, config.TLSConfig, + config.Speed.SendBPS, config.Speed.ReceiveBPS, + config.MaxReceiveWindowPerConnection, config.MaxReceiveWindow, + forwarder.CongestionFactory(config.CongestionFactory), + func(addr net.Addr, banner string, cSend uint64, cRecv uint64) { + if callbacks.ServerConnectedCallback != nil { + callbacks.ServerConnectedCallback(addr, banner, cSend, cRecv) + } + }, + func(err error) { + if callbacks.ServerErrorCallback != nil { + callbacks.ServerErrorCallback(err) + } + }, + func(addr net.Addr) { + if callbacks.NewTCPConnectionCallback != nil { + callbacks.NewTCPConnectionCallback(addr) + } + }, + func(addr net.Addr, err error) { + if callbacks.TCPConnectionClosedCallback != nil { + callbacks.TCPConnectionClosedCallback(addr, err) + } + }, + ) + if err != nil { + return nil, err + } + return &client{qc: qc}, nil +} + +func (c *client) Stats() Stats { + addr, in, out := c.qc.Stats() + return Stats{ + RemoteAddr: addr, + inboundBytes: in, + outboundBytes: out, + } +} + +func (c *client) Close() error { + return c.Close() +} diff --git a/pkg/forwarder/interface.go b/pkg/forwarder/interface.go new file mode 100644 index 0000000..758acfb --- /dev/null +++ b/pkg/forwarder/interface.go @@ -0,0 +1,89 @@ +package forwarder + +import ( + "crypto/tls" + "github.com/tobyxdd/hysteria/internal/forwarder" + "net" +) + +type CongestionFactory forwarder.CongestionFactory + +// A server can support multiple forwarding entries (listenAddr/remoteAddr pairs) +type Server interface { + Add(listenAddr, remoteAddr string) error + Remove(listenAddr string) error + Stats() map[string]Stats +} + +// An empty ServerConfig is a valid one +type ServerConfig struct { + // A banner message that will be sent to the client after the connection is established. + // No message if not set. + BannerMessage string + // TLSConfig is used to configure the TLS server. + // Use an insecure self-signed certificate if not set. + TLSConfig *tls.Config + // MaxSpeedPerClient is the maximum allowed sending and receiving speed for each client. + // Sending speed will never exceed this limit, even if a client demands a larger value. + // No restrictions if not set. + MaxSpeedPerClient *Speed + // Corresponds to MaxReceiveStreamFlowControlWindow in QUIC. + MaxReceiveWindowPerConnection uint64 + // Corresponds to MaxReceiveConnectionFlowControlWindow in QUIC. + MaxReceiveWindowPerClient uint64 + // Max number of simultaneous connections allowed for a client + MaxConnectionPerClient int + // Congestion factory + CongestionFactory CongestionFactory +} + +type ServerCallbacks struct { + ClientConnectedCallback func(listenAddr string, clientAddr net.Addr, name string, sSend uint64, sRecv uint64) + ClientDisconnectedCallback func(listenAddr string, clientAddr net.Addr, name string, err error) + ClientNewStreamCallback func(listenAddr string, clientAddr net.Addr, name string, id int) + ClientStreamClosedCallback func(listenAddr string, clientAddr net.Addr, name string, id int, err error) + TCPErrorCallback func(listenAddr string, remoteAddr string, err error) +} + +// A client supports one forwarding entry +type Client interface { + Stats() Stats + Close() error +} + +// An empty ClientConfig is NOT a valid one, as Speed must be set +type ClientConfig struct { + // A client can report its name to the server after the connection is established. + // No name if not set. + Name string + // TLSConfig is used to configure the TLS client. + // Use default settings if not set. + TLSConfig *tls.Config + // Speed reported by the client when negotiating with the server. + // The actual speed will also depend on the configuration of the server. + Speed *Speed + // Corresponds to MaxReceiveStreamFlowControlWindow in QUIC. + MaxReceiveWindowPerConnection uint64 + // Corresponds to MaxReceiveConnectionFlowControlWindow in QUIC. + MaxReceiveWindow uint64 + // Congestion factory + CongestionFactory CongestionFactory +} + +type ClientCallbacks struct { + ServerConnectedCallback func(addr net.Addr, banner string, cSend uint64, cRecv uint64) + ServerErrorCallback func(err error) + NewTCPConnectionCallback func(addr net.Addr) + TCPConnectionClosedCallback func(addr net.Addr, err error) +} + +type Speed struct { + SendBPS uint64 + ReceiveBPS uint64 +} + +type Stats struct { + RemoteAddr string + inboundBytes uint64 + outboundBytes uint64 +} diff --git a/pkg/forwarder/params.go b/pkg/forwarder/params.go new file mode 100644 index 0000000..ffa6d90 --- /dev/null +++ b/pkg/forwarder/params.go @@ -0,0 +1,9 @@ +package forwarder + +const ( + TLSAppProtocol = "hysteria-forwarder" + + defaultReceiveWindowConn = 33554432 + defaultReceiveWindow = 67108864 + defaultMaxClientConn = 100 +) diff --git a/pkg/forwarder/server.go b/pkg/forwarder/server.go new file mode 100644 index 0000000..e05d34b --- /dev/null +++ b/pkg/forwarder/server.go @@ -0,0 +1,119 @@ +package forwarder + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "github.com/tobyxdd/hysteria/internal/forwarder" + "math/big" + "net" +) + +type server struct { + config ServerConfig + callbacks ServerCallbacks + entries map[string]*forwarder.QUICServer +} + +func NewServer(config ServerConfig, callbacks ServerCallbacks) Server { + // Fix config first + if config.TLSConfig == nil { + config.TLSConfig = generateInsecureTLSConfig() + } + if config.MaxSpeedPerClient == nil { + config.MaxSpeedPerClient = &Speed{0, 0} + } + if config.MaxReceiveWindowPerConnection == 0 { + config.MaxReceiveWindowPerConnection = defaultReceiveWindowConn + } + if config.MaxReceiveWindowPerClient == 0 { + config.MaxReceiveWindowPerClient = defaultReceiveWindow + } + if config.MaxConnectionPerClient <= 0 { + config.MaxConnectionPerClient = defaultMaxClientConn + } + return &server{config: config, callbacks: callbacks, entries: make(map[string]*forwarder.QUICServer)} +} + +func (s *server) Add(listenAddr, remoteAddr string) error { + qs, err := forwarder.NewQUICServer(listenAddr, remoteAddr, s.config.BannerMessage, s.config.TLSConfig, + s.config.MaxSpeedPerClient.SendBPS, s.config.MaxSpeedPerClient.ReceiveBPS, + s.config.MaxReceiveWindowPerConnection, s.config.MaxReceiveWindowPerClient, + s.config.MaxConnectionPerClient, forwarder.CongestionFactory(s.config.CongestionFactory), + func(addr net.Addr, name string, sSend uint64, sRecv uint64) { + if s.callbacks.ClientConnectedCallback != nil { + s.callbacks.ClientConnectedCallback(listenAddr, addr, name, sSend, sRecv) + } + }, + func(addr net.Addr, name string, err error) { + if s.callbacks.ClientDisconnectedCallback != nil { + s.callbacks.ClientDisconnectedCallback(listenAddr, addr, name, err) + } + }, + func(addr net.Addr, name string, id int) { + if s.callbacks.ClientNewStreamCallback != nil { + s.callbacks.ClientNewStreamCallback(listenAddr, addr, name, id) + } + }, + func(addr net.Addr, name string, id int, err error) { + if s.callbacks.ClientStreamClosedCallback != nil { + s.callbacks.ClientStreamClosedCallback(listenAddr, addr, name, id, err) + } + }, + func(remoteAddr string, err error) { + if s.callbacks.TCPErrorCallback != nil { + s.callbacks.TCPErrorCallback(listenAddr, remoteAddr, err) + } + }, + ) + if err != nil { + return err + } + s.entries[listenAddr] = qs + return nil +} + +func (s *server) Remove(listenAddr string) error { + defer delete(s.entries, listenAddr) + if qs, ok := s.entries[listenAddr]; ok && qs != nil { + return qs.Close() + } + return nil +} + +func (s *server) Stats() map[string]Stats { + r := make(map[string]Stats, len(s.entries)) + for laddr, sv := range s.entries { + addr, in, out := sv.Stats() + r[laddr] = Stats{ + RemoteAddr: addr, + inboundBytes: in, + outboundBytes: out, + } + } + return r +} + +func generateInsecureTLSConfig() *tls.Config { + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + panic(err) + } + template := x509.Certificate{SerialNumber: big.NewInt(1)} + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + panic(err) + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + panic(err) + } + return &tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + NextProtos: []string{TLSAppProtocol}, + } +}