支持 QUIC 协议中转

This commit is contained in:
2025-06-25 11:33:11 +08:00
parent c6050d6824
commit 0e5e0ced30
7 changed files with 394 additions and 15 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.vscode
config.json
/gateway
/logs/

161
cmd/client/main.go Normal file
View File

@@ -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()
}

View File

@@ -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()).

View File

@@ -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")
}

122
cmd/gateway/quic.go Normal file
View File

@@ -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
}

21
go.mod
View File

@@ -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
)

55
go.sum
View File

@@ -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=