diff --git a/README.md b/README.md index 85295b0..3aa4824 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ A simple VPN written in Go. * VPN over kcp * VPN over utls * VPN over dtls +* VPN over h2 # Usage ``` @@ -46,7 +47,7 @@ Usage of vtun: -obfs enable data obfuscation -p string - protocol udp/tls/grpc/quic/utls/dtls/ws/wss (default "udp") + protocol udp/tls/grpc/quic/utls/dtls/h2/ws/wss (default "udp") -path string websocket path (default "/freedom") -privatekey string diff --git a/README_CN.md b/README_CN.md index b9ea8aa..c1cf8cf 100644 --- a/README_CN.md +++ b/README_CN.md @@ -19,6 +19,7 @@ * 支持kcp * 支持utls * 支持dtls +* 支持h2 # 用法 @@ -47,7 +48,7 @@ Usage of vtun: -obfs enable data obfuscation -p string - protocol udp/tls/grpc/quic/utls/dtls/ws/wss (default "udp") + protocol udp/tls/grpc/quic/utls/dtls/h2/ws/wss (default "udp") -path string websocket path (default "/freedom") -privatekey string diff --git a/app/app.go b/app/app.go index 454fa30..b167374 100644 --- a/app/app.go +++ b/app/app.go @@ -2,6 +2,7 @@ package app import ( "github.com/net-byte/vtun/dtls" + "github.com/net-byte/vtun/h2" "github.com/net-byte/vtun/kcp" "github.com/net-byte/vtun/quic" "github.com/net-byte/vtun/utls" @@ -111,6 +112,12 @@ func (app *App) StartApp() { } else { dtls.StartClient(app.Iface, *app.Config) } + case "h2": + if app.Config.ServerMode { + h2.StartServer(app.Iface, *app.Config) + } else { + h2.StartClient(app.Iface, *app.Config) + } default: if app.Config.ServerMode { udp.StartServer(app.Iface, *app.Config) diff --git a/h2/h2client.go b/h2/h2client.go new file mode 100644 index 0000000..2dcc6e2 --- /dev/null +++ b/h2/h2client.go @@ -0,0 +1,145 @@ +package h2 + +import ( + "context" + "crypto/tls" + "fmt" + "github.com/golang/snappy" + "github.com/net-byte/vtun/common/cache" + "github.com/net-byte/vtun/common/cipher" + "github.com/net-byte/vtun/common/config" + "github.com/net-byte/vtun/common/counter" + "github.com/net-byte/vtun/common/netutil" + "github.com/net-byte/water" + "golang.org/x/net/http2" + "io" + "log" + "net/http" + "time" +) + +// StartClient starts the h2 client +func StartClient(iFace *water.Interface, config config.Config) { + log.Println("vtun h2 client started") + go tunToH2(config, iFace) + tlsconfig := &tls.Config{ + InsecureSkipVerify: config.TLSInsecureSkipVerify, + } + if config.TLSSni != "" { + tlsconfig.ServerName = config.TLSSni + } + client := &Client{ + Client: &http.Client{ + Transport: &http2.Transport{ + TLSClientConfig: tlsconfig, + }, + }, + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + for { + conn, resp, err := client.Connect(ctx, fmt.Sprintf("https://%s%s", config.ServerAddr, config.WebSocketPath)) + if err != nil { + log.Fatalf("Initiate conn: %s\n", err) + } + defer conn.Close() + if resp.StatusCode != http.StatusOK { + log.Fatalf("bad status code: %d\n", resp.StatusCode) + } + cache.GetCache().Set("h2conn", conn, 24*time.Hour) + h2ToTun(config, conn, iFace) + cache.GetCache().Delete("h2conn") + conn.Close() + } +} + +// tunToH2 sends packets from tun to h2 +func tunToH2(config config.Config, iFace *water.Interface) { + packet := make([]byte, config.BufferSize) + for { + n, err := iFace.Read(packet) + if err != nil { + netutil.PrintErr(err, config.Verbose) + break + } + if v, ok := cache.GetCache().Get("h2conn"); ok { + b := packet[:n] + if config.Obfs { + b = cipher.XOR(b) + } + if config.Compress { + b = snappy.Encode(nil, b) + } + conn := v.(*Conn) + n, err = conn.Write(b) + if err != nil { + netutil.PrintErr(err, config.Verbose) + continue + } + counter.IncrWrittenBytes(n) + } + } +} + +// h2ToTun sends packets from h2 to tun +func h2ToTun(config config.Config, conn *Conn, iFace *water.Interface) { + packet := make([]byte, config.BufferSize) + for { + n, err := conn.Read(packet) + if err != nil { + netutil.PrintErr(err, config.Verbose) + break + } + b := packet[:n] + if config.Compress { + b, err = snappy.Decode(nil, b) + if err != nil { + netutil.PrintErr(err, config.Verbose) + break + } + } + if config.Obfs { + b = cipher.XOR(b) + } + _, err = iFace.Write(b) + if err != nil { + netutil.PrintErr(err, config.Verbose) + break + } + counter.IncrReadBytes(len(b)) + } +} + +type Client struct { + Method string + Header http.Header + Client *http.Client +} + +func (c *Client) Connect(ctx context.Context, urlStr string) (*Conn, *http.Response, error) { + reader, writer := io.Pipe() + req, err := http.NewRequest(c.Method, urlStr, reader) + if err != nil { + return nil, nil, err + } + if c.Header != nil { + req.Header = c.Header + } + req = req.WithContext(ctx) + httpClient := c.Client + if httpClient == nil { + httpClient = defaultClient.Client + } + resp, err := httpClient.Do(req) + if err != nil { + return nil, nil, err + } + conn, ctx := newConn(req.Context(), resp.Body, writer) + resp.Request = req.WithContext(ctx) + return conn, resp, nil +} + +var defaultClient = Client{ + Method: http.MethodPost, + Client: &http.Client{Transport: &http2.Transport{}}, +} diff --git a/h2/h2conn.go b/h2/h2conn.go new file mode 100644 index 0000000..dd62a6c --- /dev/null +++ b/h2/h2conn.go @@ -0,0 +1,41 @@ +package h2 + +import ( + "context" + "io" + "sync" +) + +type Conn struct { + r io.Reader + wc io.WriteCloser + cancel context.CancelFunc + wLock sync.Mutex + rLock sync.Mutex +} + +func newConn(ctx context.Context, r io.Reader, wc io.WriteCloser) (*Conn, context.Context) { + ctx, cancel := context.WithCancel(ctx) + return &Conn{ + r: r, + wc: wc, + cancel: cancel, + }, ctx +} + +func (c *Conn) Write(data []byte) (int, error) { + c.wLock.Lock() + defer c.wLock.Unlock() + return c.wc.Write(data) +} + +func (c *Conn) Read(data []byte) (int, error) { + c.rLock.Lock() + defer c.rLock.Unlock() + return c.r.Read(data) +} + +func (c *Conn) Close() error { + c.cancel() + return c.wc.Close() +} diff --git a/h2/h2server.go b/h2/h2server.go new file mode 100644 index 0000000..b8bdc03 --- /dev/null +++ b/h2/h2server.go @@ -0,0 +1,144 @@ +package h2 + +import ( + "fmt" + "github.com/golang/snappy" + "github.com/net-byte/vtun/common/cache" + "github.com/net-byte/vtun/common/cipher" + "github.com/net-byte/vtun/common/config" + "github.com/net-byte/vtun/common/counter" + "github.com/net-byte/vtun/common/netutil" + "github.com/net-byte/water" + "io" + "log" + "net/http" + "time" +) + +// StartServer starts the h2 server +func StartServer(iFace *water.Interface, config config.Config) { + log.Printf("vtun h2 server started on %v", config.LocalAddr) + mux := http.NewServeMux() + mux.Handle(config.WebSocketPath, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + ServeHTTP(writer, request, config, iFace) + })) + srv := &http.Server{ + Addr: config.LocalAddr, + Handler: mux, + } + go toClient(config, iFace) + log.Fatal(srv.ListenAndServeTLS(config.TLSCertificateFilePath, config.TLSCertificateKeyFilePath)) +} + +func ServeHTTP(w http.ResponseWriter, r *http.Request, config config.Config, iFace *water.Interface) { + conn, err := Accept(w, r) + if err != nil { + log.Printf("Failed creating connection from %s: %s", r.RemoteAddr, err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + defer conn.Close() + toServer(conn, config, iFace) +} + +// toClient sends packets from tun to h2 +func toClient(config config.Config, iFace *water.Interface) { + packet := make([]byte, config.BufferSize) + for { + n, err := iFace.Read(packet) + if err != nil { + netutil.PrintErr(err, config.Verbose) + break + } + b := packet[:n] + if key := netutil.GetDstKey(b); key != "" { + if v, ok := cache.GetCache().Get(key); ok { + if config.Obfs { + b = cipher.XOR(b) + } + if config.Compress { + b = snappy.Encode(nil, b) + } + _, err = v.(*Conn).Write(b) + if err != nil { + cache.GetCache().Delete(key) + continue + } + counter.IncrWrittenBytes(n) + } + } + } +} + +// toServer sends packets from h2 to tun +func toServer(conn *Conn, config config.Config, iFace *water.Interface) { + packet := make([]byte, config.BufferSize) + for { + n, err := conn.Read(packet) + if err != nil { + netutil.PrintErr(err, config.Verbose) + break + } + b := packet[:n] + if config.Compress { + b, err = snappy.Decode(nil, b) + if err != nil { + netutil.PrintErr(err, config.Verbose) + break + } + } + if config.Obfs { + b = cipher.XOR(b) + } + if key := netutil.GetSrcKey(b); key != "" { + cache.GetCache().Set(key, conn, 24*time.Hour) + iFace.Write(b) + counter.IncrReadBytes(len(b)) + } + } +} + +var ErrHTTP2NotSupported = fmt.Errorf("HTTP2 not supported") + +type Server struct { + StatusCode int +} + +func (u *Server) Accept(w http.ResponseWriter, r *http.Request) (*Conn, error) { + if !r.ProtoAtLeast(2, 0) { + return nil, ErrHTTP2NotSupported + } + flusher, ok := w.(http.Flusher) + if !ok { + return nil, ErrHTTP2NotSupported + } + c, ctx := newConn(r.Context(), r.Body, &flushWrite{w: w, f: flusher}) + *r = *r.WithContext(ctx) + w.WriteHeader(u.StatusCode) + flusher.Flush() + + return c, nil +} + +var defaultUpgrade = Server{ + StatusCode: http.StatusOK, +} + +func Accept(w http.ResponseWriter, r *http.Request) (*Conn, error) { + return defaultUpgrade.Accept(w, r) +} + +type flushWrite struct { + w io.Writer + f http.Flusher +} + +func (w *flushWrite) Write(data []byte) (int, error) { + n, err := w.w.Write(data) + w.f.Flush() + return n, err +} + +func (w *flushWrite) Close() error { + return nil +} diff --git a/main.go b/main.go index 7bb6d2c..bb70cc9 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ func main() { flag.StringVar(&config.ServerIP, "sip", "172.16.0.1", "server ip") flag.StringVar(&config.ServerIPv6, "sip6", "fced:9999::1", "server ipv6") flag.StringVar(&config.Key, "k", "freedom@2023", "key") - flag.StringVar(&config.Protocol, "p", "udp", "protocol udp/tls/grpc/quic/utls/dtls/ws/wss") + flag.StringVar(&config.Protocol, "p", "udp", "protocol udp/tls/grpc/quic/utls/dtls/h2/ws/wss") flag.StringVar(&config.WebSocketPath, "path", "/freedom", "websocket path") flag.BoolVar(&config.ServerMode, "S", false, "server mode") flag.BoolVar(&config.GlobalMode, "g", false, "client global mode")