diff --git a/.gitignore b/.gitignore index a930474..db565b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.vscode + config.json /gateway /logs/ diff --git a/cmd/client/main.go b/cmd/client/main.go new file mode 100644 index 0000000..2f71be5 --- /dev/null +++ b/cmd/client/main.go @@ -0,0 +1,161 @@ +package main + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "os" + "strings" + "sync" + "time" + + "github.com/quic-go/quic-go" + "github.com/rs/zerolog/log" +) + +var ( + localPort = 25565 + + mcHost = "bh.mc.tursom.cn" + mcPort = 25565 +) + +func main() { + if len(os.Args) > 1 { + mcHost = os.Args[1] + } + + // 监听TCP端口 + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", localPort)) + if err != nil { + log.Fatal().Err(err). + Int("port", localPort). + Msg("Failed to listen on port") + } + defer listener.Close() + log.Info(). + Int("port", localPort). + Msg("Listening for TCP connections") + + for { + // 接受传入的连接 + conn, err := listener.Accept() + if err != nil { + log.Err(err).Msg("Error accepting") + continue + } + log.Info(). + Str("client", conn.RemoteAddr().String()). + Int("port", localPort). + Msg("Accepted connection") + // 处理连接 + go handlerConn(conn) + } +} + +func handlerConn(conn net.Conn) { + defer conn.Close() + + tlsConf := &tls.Config{ + InsecureSkipVerify: true, // 跳过证书检查 + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) // 3s handshake timeout + defer cancel() + + quicConn, err := quic.DialAddr(ctx, fmt.Sprintf("%s:%d", mcHost, mcPort), tlsConf, nil) + if err != nil { + log.Err(err).Msg("Failed to dial QUIC") + return + } + + stream, err := quicConn.OpenStream() + if err != nil { + log.Err(err).Msg("Failed to open stream") + return + } + defer stream.Close() + log.Info(). + Msg("QUIC stream opened") + + // read and write stream data + + buf := make([]byte, 1024) + n, err := conn.Read(buf) + if err != nil { + log.Err(err). + Str("client", conn.RemoteAddr().String()). + Msg("Error reading hostname") + return + } + if n == 0 { + log.Err(errors.New("empty buffer")). + Str("client", conn.RemoteAddr().String()). + Msg("Error: buffer is empty") + return + } + buf = buf[:n] + + new_buf := replaceMcHost(buf, mcHost) + _, err = stream.Write(new_buf) // 写入数据到 QUIC 流 + if err != nil { + log.Err(err). + Str("client", conn.RemoteAddr().String()). + Str("host", mcHost). + Int("port", mcPort). + Msg("Error writing to QUIC stream") + return + } + + var wg sync.WaitGroup + wg.Add(1) + go copyData(stream, conn, &wg) + copyData(conn, stream, nil) + wg.Wait() +} + +func copyData(src io.Reader, dst io.Writer, wg *sync.WaitGroup) { + if wg != nil { + defer wg.Done() + } + + _, err := io.Copy(dst, src) + if err != nil && err != io.EOF { + log.Err(err).Msg("Error copying data") + } +} + +func replaceMcHost(buf []byte, host string) []byte { + if len(buf) < 5 { + return nil + } + + var out bytes.Buffer + head := buf[:4] + + buf = buf[4:] + host_len := buf[0] + if len(buf) < int(host_len)+1 { + return nil + } + + raw_host := string(buf[1 : host_len+1]) + if spliterIndex := strings.IndexRune(raw_host, 0); spliterIndex != -1 { + host = host + raw_host[spliterIndex:] + } + + // 修改标识数据包长度的字节 + head[0] += byte(len(host) - len(raw_host)) + + out.Write(head) // 保留前四个字节 + out.WriteByte(byte(len(host))) // 写入主机名长度 + out.Write([]byte(host)) // 写入主机名 + + out.Write(buf[host_len+1:]) // 写入剩余数据 + + return out.Bytes() +} diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index 0ed4009..ecc3f35 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -21,6 +21,8 @@ import ( type ( Config struct { Port int `json:"port"` + Tcp bool `json:"tcp"` + Quic bool `json:"quic"` Hosts map[string]string `json:"hosts"` Default string `json:"default"` Log LogConfig `json:"log"` @@ -33,6 +35,8 @@ type ( ) var ( + configFile = "config.json" + config Config currentLogFile string configLoadLock sync.Mutex @@ -42,7 +46,7 @@ func loadConfig() error { configLoadLock.Lock() defer configLoadLock.Unlock() - file, err := os.Open("config.json") + file, err := os.Open(configFile) if err != nil { return err } @@ -103,11 +107,14 @@ func watchConfig() *fsnotify.Watcher { go func() { for { select { - case _, ok := <-watcher.Events: + case event, ok := <-watcher.Events: if !ok { log.Error().Msg("watcher.Events channel closed") return } + if !strings.HasSuffix(event.Name, configFile) { + continue + } case err, ok := <-watcher.Errors: if !ok { log.Error().Msg("watcher.Errors channel closed") @@ -145,15 +152,37 @@ func main() { go handleLogRotate() + var wg sync.WaitGroup + defer wg.Wait() + + // 启动QUIC服务 + if config.Quic { + wg.Add(1) + go runQuic(&wg) + } + // 监听TCP端口 + if config.Tcp { + wg.Add(1) + go runTcp(&wg) + } +} + +func runTcp(wg *sync.WaitGroup) { + if wg != nil { + defer wg.Done() + } + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", config.Port)) if err != nil { - panic(err) + log.Fatal().Err(err). + Int("port", config.Port). + Msg("Failed to listen on port") } defer listener.Close() log.Info(). Int("port", config.Port). - Msg("Listening") + Msg("Listening for TCP connections") for { // 接受传入的连接 @@ -209,6 +238,13 @@ func handleRequest(conn net.Conn) { if !ok { host = config.Default } + if host == "" { + log.Err(errEmptyBuffer). + Str("client", conn.RemoteAddr().String()). + Str("host", mc_host). + Msg("failed to route host") + return + } log.Info(). Str("client", conn.RemoteAddr().String()). diff --git a/cmd/gateway/pid_linux.go b/cmd/gateway/pid_linux.go index 911f474..ac34643 100644 --- a/cmd/gateway/pid_linux.go +++ b/cmd/gateway/pid_linux.go @@ -7,9 +7,9 @@ import ( func writePIDFile() error { pid := os.Getpid() - return os.WriteFile("/var/run/mc-gateway.pid", []byte(fmt.Sprintf("%d\n", pid)), 0644) + return os.WriteFile("/dev/shm/mc-gateway.pid", []byte(fmt.Sprintf("%d\n", pid)), 0644) } func removePIDFile() { - os.Remove("/var/run/mc-gateway.pid") + os.Remove("/dev/shm/mc-gateway.pid") } diff --git a/cmd/gateway/quic.go b/cmd/gateway/quic.go new file mode 100644 index 0000000..366beef --- /dev/null +++ b/cmd/gateway/quic.go @@ -0,0 +1,122 @@ +package main + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "sync" + "time" + + quic "github.com/quic-go/quic-go" + "github.com/rs/zerolog/log" +) + +type quicConn struct { + quic.Connection + quic.Stream +} + +func runQuic(wg *sync.WaitGroup) { + if wg != nil { + defer wg.Done() + } + + udpConn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: config.Port}) + if err != nil { + log.Panic().Err(err).Msg("Failed to listen UDP") + } + defer udpConn.Close() + + tlsConf, err := generateTLSConfig() + if err != nil { + log.Panic().Err(err).Msg("Failed to generate TLS config") + } + + ln, err := quic.Listen(udpConn, tlsConf, nil) + if err != nil { + log.Panic().Err(err).Msg("Failed to listen QUIC") + } + log.Info().Int("port", config.Port).Msg("Listening for QUIC connections") + + for { + conn, err := ln.Accept(context.Background()) + if err != nil { + log.Err(err).Msg("Error accepting QUIC connection") + continue + } + + go handleQuicRequest(conn) + } +} + +func handleQuicRequest(conn quic.Connection) { + defer conn.CloseWithError(0, "Closing connection") + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + stream, err := conn.AcceptStream(ctx) + if err != nil { + log.Err(err).Msg("Error accepting stream") + return + } + + handleRequest(quicConn{ + Connection: conn, + Stream: stream, + }) +} + +func generateTLSConfig() (*tls.Config, error) { + // 生成私钥 + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + // 创建证书模板 + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Example Org"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), // 有效期 1 年 + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + // 自签名证书 + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, err + } + + // 编码证书和私钥 + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + keyPEM, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, err + } + keyPEMBlock := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyPEM}) + + // 加载到 tls.Certificate + cert, err := tls.X509KeyPair(certPEM, keyPEMBlock) + if err != nil { + return nil, err + } + + // 返回 tls.Config + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, nil +} diff --git a/go.mod b/go.mod index 147e70b..6b2a517 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,23 @@ module github.com/tursom/mc-gateway go 1.23.1 -require github.com/fsnotify/fsnotify v1.7.0 +require ( + github.com/fsnotify/fsnotify v1.7.0 + github.com/quic-go/quic-go v0.52.0 + github.com/rs/zerolog v1.33.0 +) require ( + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect -) - -require ( - github.com/rs/zerolog v1.33.0 - golang.org/x/sys v0.12.0 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/tools v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 81a4eb6..e5fe4b1 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,68 @@ +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/pkg/errors v0.9.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/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA= +github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=