├── version └── version.go ├── go.mod ├── verbose_logger └── logger.go ├── main └── main.go ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── ci.yml ├── backoff └── backoff.go ├── util ├── duplex_conn.go └── util.go ├── LICENSE ├── openssl_aes_ctr_duplex ├── openssl_aes_ctr_duplex.go └── openssl_aes_ctr.go ├── piping_util ├── piping_util.go ├── piping_tunnel_util.go └── duplex.go ├── .goreleaser.yml ├── openpgp_duplex └── openpgp.go ├── hb_duplex └── hb_duplex.go ├── aes_ctr_duplex └── aes_ctr_duplex.go ├── early_piping_duplex └── duplex.go ├── cmd ├── root.go ├── shared.go ├── socks │ └── socks.go ├── server │ └── server.go └── client │ └── client.go ├── io_progress └── io_progress.go ├── CHANGELOG.md ├── README.md ├── go.sum └── pmux └── pmux.go /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | const Version = "0.12.0" 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nwtgck/go-piping-tunnel 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/hashicorp/yamux v0.1.2 7 | github.com/mattn/go-isatty v0.0.20 // indirect 8 | github.com/mattn/go-tty v0.0.5 9 | github.com/nwtgck/go-socks v0.1.0 10 | github.com/pkg/errors v0.9.1 11 | github.com/spf13/cobra v1.8.1 12 | golang.org/x/crypto v0.25.0 13 | ) 14 | -------------------------------------------------------------------------------- /verbose_logger/logger.go: -------------------------------------------------------------------------------- 1 | package verbose_logger 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Logger struct { 8 | Level int 9 | } 10 | 11 | func (l *Logger) Log(messages ...string) { 12 | var idx int 13 | last := len(messages) - 1 14 | if l.Level < last { 15 | idx = l.Level 16 | } else { 17 | idx = last 18 | } 19 | fmt.Println(messages[idx]) 20 | } 21 | -------------------------------------------------------------------------------- /main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/nwtgck/go-piping-tunnel/cmd" 6 | _ "github.com/nwtgck/go-piping-tunnel/cmd/client" 7 | _ "github.com/nwtgck/go-piping-tunnel/cmd/server" 8 | _ "github.com/nwtgck/go-piping-tunnel/cmd/socks" 9 | "os" 10 | ) 11 | 12 | func main() { 13 | if err := cmd.RootCmd.Execute(); err != nil { 14 | _, _ = fmt.Fprintf(os.Stderr, err.Error()) 15 | os.Exit(-1) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | timezone: Asia/Tokyo 8 | open-pull-requests-limit: 99 9 | reviewers: [ nwtgck ] 10 | assignees: [ nwtgck ] 11 | - package-ecosystem: github-actions 12 | directory: "/" 13 | schedule: 14 | interval: daily 15 | timezone: Asia/Tokyo 16 | open-pull-requests-limit: 99 17 | reviewers: [ nwtgck ] 18 | assignees: [ nwtgck ] 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: '1.20' 19 | - name: Run GoReleaser 20 | uses: goreleaser/goreleaser-action@v5 21 | with: 22 | version: v1.19.2 23 | args: release --rm-dist 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 26 | -------------------------------------------------------------------------------- /backoff/backoff.go: -------------------------------------------------------------------------------- 1 | package backoff 2 | 3 | import "time" 4 | 5 | type exponentialBackoff struct { 6 | initialDuration time.Duration 7 | currentDuration time.Duration 8 | maxDuration time.Duration 9 | } 10 | 11 | func NewExponentialBackoff() *exponentialBackoff { 12 | initialDuration := 500 * time.Millisecond 13 | return &exponentialBackoff{ 14 | initialDuration: initialDuration, 15 | currentDuration: initialDuration, 16 | maxDuration: 1 * time.Minute, 17 | } 18 | } 19 | 20 | func (b *exponentialBackoff) NextDuration() time.Duration { 21 | d := b.currentDuration 22 | nextDuration := time.Duration(float64(b.currentDuration) * 1.5) 23 | if nextDuration < b.maxDuration { 24 | b.currentDuration = nextDuration 25 | } 26 | return d 27 | } 28 | 29 | func (b *exponentialBackoff) Reset() { 30 | b.currentDuration = b.initialDuration 31 | } 32 | -------------------------------------------------------------------------------- /util/duplex_conn.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "time" 7 | ) 8 | 9 | type duplexConn struct { 10 | duplex io.ReadWriteCloser 11 | } 12 | 13 | func NewDuplexConn(d io.ReadWriteCloser) *duplexConn { 14 | return &duplexConn{duplex: d} 15 | } 16 | 17 | func (d *duplexConn) Read(p []byte) (int, error) { 18 | return d.duplex.Read(p) 19 | } 20 | 21 | func (d *duplexConn) Write(p []byte) (int, error) { 22 | return d.duplex.Write(p) 23 | } 24 | 25 | func (d *duplexConn) Close() error { 26 | return d.duplex.Close() 27 | } 28 | 29 | func (d *duplexConn) LocalAddr() net.Addr { 30 | return nil 31 | } 32 | 33 | func (d *duplexConn) RemoteAddr() net.Addr { 34 | return nil 35 | } 36 | 37 | func (d *duplexConn) SetDeadline(t time.Time) error { 38 | return nil 39 | } 40 | 41 | func (d *duplexConn) SetReadDeadline(t time.Time) error { 42 | return nil 43 | } 44 | 45 | func (d *duplexConn) SetWriteDeadline(t time.Time) error { 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ryo Ota 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /openssl_aes_ctr_duplex/openssl_aes_ctr_duplex.go: -------------------------------------------------------------------------------- 1 | package openssl_aes_ctr_duplex 2 | 3 | import ( 4 | "github.com/nwtgck/go-piping-tunnel/util" 5 | "hash" 6 | "io" 7 | ) 8 | 9 | type opensslAesCtrDuplex struct { 10 | encryptWriter io.WriteCloser 11 | decryptedReader io.Reader 12 | closeBaseReader func() error 13 | } 14 | 15 | func Duplex(baseWriter io.WriteCloser, baseReader io.ReadCloser, passphrase []byte, pbkdf2Iter int, keyLen int, h func() hash.Hash) (*opensslAesCtrDuplex, error) { 16 | encryptWriter, err := AesCtrEncryptWithPbkdf2(baseWriter, passphrase, pbkdf2Iter, keyLen, h) 17 | if err != nil { 18 | return nil, err 19 | } 20 | decryptedReader, err := AesCtrDecryptWithPbkdf2(baseReader, passphrase, pbkdf2Iter, keyLen, h) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return &opensslAesCtrDuplex{encryptWriter: encryptWriter, decryptedReader: decryptedReader, closeBaseReader: baseReader.Close}, nil 25 | } 26 | 27 | func (d *opensslAesCtrDuplex) Write(p []byte) (int, error) { 28 | return d.encryptWriter.Write(p) 29 | } 30 | 31 | func (d *opensslAesCtrDuplex) Read(p []byte) (int, error) { 32 | return d.decryptedReader.Read(p) 33 | } 34 | 35 | func (d *opensslAesCtrDuplex) Close() error { 36 | wErr := d.encryptWriter.Close() 37 | rErr := d.closeBaseReader() 38 | return util.CombineErrors(wErr, rErr) 39 | } 40 | -------------------------------------------------------------------------------- /piping_util/piping_util.go: -------------------------------------------------------------------------------- 1 | package piping_util 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | func PipingSendWithContext(ctx context.Context, httpClient *http.Client, headers []KeyValue, uploadUrl string, reader io.Reader) (*http.Response, error) { 10 | req, err := http.NewRequestWithContext(ctx, "POST", uploadUrl, reader) 11 | if err != nil { 12 | return nil, err 13 | } 14 | req.Header.Set("Content-Type", "application/octet-stream") 15 | for _, kv := range headers { 16 | req.Header.Set(kv.Key, kv.Value) 17 | } 18 | return httpClient.Do(req) 19 | } 20 | 21 | func PipingSend(httpClient *http.Client, headers []KeyValue, uploadUrl string, reader io.Reader) (*http.Response, error) { 22 | return PipingSendWithContext(context.Background(), httpClient, headers, uploadUrl, reader) 23 | } 24 | 25 | func PipingGetWithContext(ctx context.Context, httpClient *http.Client, headers []KeyValue, downloadUrl string) (*http.Response, error) { 26 | req, err := http.NewRequestWithContext(ctx, "GET", downloadUrl, nil) 27 | if err != nil { 28 | return nil, err 29 | } 30 | for _, kv := range headers { 31 | req.Header.Set(kv.Key, kv.Value) 32 | } 33 | return httpClient.Do(req) 34 | } 35 | 36 | func PipingGet(httpClient *http.Client, headers []KeyValue, downloadUrl string) (*http.Response, error) { 37 | return PipingGetWithContext(context.Background(), httpClient, headers, downloadUrl) 38 | } 39 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: piping-tunnel 2 | builds: 3 | - env: 4 | - CGO_ENABLED=0 5 | goos: 6 | - linux 7 | - windows 8 | - darwin 9 | - freebsd 10 | goarch: 11 | - amd64 12 | - arm 13 | - arm64 14 | - 386 15 | - ppc64le 16 | - s390x 17 | - mips64 18 | - mips64le 19 | goarm: 20 | - 6 21 | - 7 22 | main: ./main/main.go 23 | archives: 24 | - name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}-{{ .Mips }}{{ end }}' 25 | format_overrides: 26 | - goos: windows 27 | format: zip 28 | nfpms: 29 | - license: MIT 30 | maintainer: Ryo Ota 31 | homepage: https://github.com/nwtgck/go-piping-tunnel 32 | description: "Tunneling from anywhere with Piping Server" 33 | formats: 34 | - rpm 35 | - deb 36 | file_name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}-{{ .Mips }}{{ end }}' 37 | checksum: 38 | name_template: 'checksums.txt' 39 | release: 40 | github: 41 | disable: false 42 | prerelease: auto 43 | name_template: "v{{.Version}}" 44 | brews: 45 | - tap: 46 | owner: nwtgck 47 | name: homebrew-piping-tunnel 48 | homepage: "https://github.com/nwtgck/go-piping-tunnel" 49 | description: "Tunneling from anywhere with Piping Server" 50 | -------------------------------------------------------------------------------- /openpgp_duplex/openpgp.go: -------------------------------------------------------------------------------- 1 | package openpgp_duplex 2 | 3 | import ( 4 | "github.com/nwtgck/go-piping-tunnel/util" 5 | "golang.org/x/crypto/openpgp" 6 | "io" 7 | ) 8 | 9 | type symmetricallyDuplex struct { 10 | encryptWriter io.WriteCloser 11 | decryptedReader io.Reader 12 | decryptedReaderCh chan interface{} // io.Reader or error 13 | closeBaseReader func() error 14 | } 15 | 16 | func SymmetricallyEncryptDuplexWithOpenPGP(baseWriter io.WriteCloser, baseReader io.ReadCloser, passphrase []byte) (*symmetricallyDuplex, error) { 17 | encryptWriter, err := openpgp.SymmetricallyEncrypt(baseWriter, passphrase, nil, nil) 18 | if err != nil { 19 | return nil, err 20 | } 21 | decryptedReaderCh := make(chan interface{}) 22 | go func() { 23 | // (base: https://github.com/golang/crypto/blob/a2144134853fc9a27a7b1e3eb4f19f1a76df13c9/openpgp/write_test.go#L129) 24 | md, err := openpgp.ReadMessage(baseReader, nil, func(keys []openpgp.Key, symmetric bool) ([]byte, error) { 25 | return passphrase, nil 26 | }, nil) 27 | if err != nil { 28 | decryptedReaderCh <- err 29 | return 30 | } 31 | decryptedReaderCh <- md.UnverifiedBody 32 | }() 33 | 34 | return &symmetricallyDuplex{ 35 | encryptWriter: encryptWriter, 36 | decryptedReaderCh: decryptedReaderCh, 37 | closeBaseReader: baseReader.Close, 38 | }, nil 39 | } 40 | 41 | func (o *symmetricallyDuplex) Write(p []byte) (int, error) { 42 | return o.encryptWriter.Write(p) 43 | } 44 | 45 | func (o *symmetricallyDuplex) Read(p []byte) (int, error) { 46 | if o.decryptedReaderCh != nil { 47 | // Get io.Reader or error 48 | result := <-o.decryptedReaderCh 49 | // If result is error 50 | if err, ok := result.(error); ok { 51 | return 0, err 52 | } 53 | o.decryptedReader = result.(io.Reader) 54 | o.decryptedReaderCh = nil 55 | } 56 | return o.decryptedReader.Read(p) 57 | } 58 | 59 | func (o *symmetricallyDuplex) Close() error { 60 | wErr := o.encryptWriter.Close() 61 | rErr := o.closeBaseReader() 62 | return util.CombineErrors(wErr, rErr) 63 | } 64 | -------------------------------------------------------------------------------- /piping_util/piping_tunnel_util.go: -------------------------------------------------------------------------------- 1 | package piping_util 2 | 3 | import ( 4 | "github.com/nwtgck/go-piping-tunnel/io_progress" 5 | "github.com/pkg/errors" 6 | "io" 7 | "net/http" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | CipherTypeOpenpgp string = "openpgp" 14 | CipherTypeAesCtr = "aes-ctr" 15 | CipherTypeOpensslAes128Ctr = "openssl-aes-128-ctr" 16 | CipherTypeOpensslAes256Ctr = "openssl-aes-256-ctr" 17 | ) 18 | 19 | type KeyValue struct { 20 | Key string 21 | Value string 22 | } 23 | 24 | func ParseKeyValueStrings(strKeyValues []string) ([]KeyValue, error) { 25 | var keyValues []KeyValue 26 | for _, str := range strKeyValues { 27 | splitted := strings.SplitN(str, ":", 2) 28 | if len(splitted) != 2 { 29 | return nil, errors.Errorf("invalid header format '%s'", str) 30 | } 31 | keyValues = append(keyValues, KeyValue{Key: splitted[0], Value: splitted[1]}) 32 | } 33 | return keyValues, nil 34 | } 35 | 36 | // NOTE: duplex is usually conn 37 | func HandleDuplex(httpClient *http.Client, duplex io.ReadWriteCloser, headers []KeyValue, uploadUrl string, downloadUrl string, downloadBufSize uint, arriveCh chan<- struct{}, showProgress bool, makeProgressMessage func(progress *io_progress.IOProgress) string) error { 38 | var progress *io_progress.IOProgress = nil 39 | if showProgress { 40 | progress = io_progress.NewIOProgress(duplex, duplex, os.Stderr, makeProgressMessage) 41 | } 42 | var reader io.Reader = duplex 43 | if progress != nil { 44 | reader = progress 45 | } 46 | _, err := PipingSend(httpClient, headers, uploadUrl, reader) 47 | if err != nil { 48 | return err 49 | } 50 | res, err := PipingGet(httpClient, headers, downloadUrl) 51 | if err != nil { 52 | return err 53 | } 54 | if arriveCh != nil { 55 | arriveCh <- struct{}{} 56 | } 57 | var writer io.Writer = duplex 58 | if progress != nil { 59 | writer = progress 60 | } 61 | var buf = make([]byte, downloadBufSize) 62 | _, err = io.CopyBuffer(writer, res.Body, buf) 63 | return err 64 | } 65 | -------------------------------------------------------------------------------- /piping_util/duplex.go: -------------------------------------------------------------------------------- 1 | package piping_util 2 | 3 | import ( 4 | "github.com/nwtgck/go-piping-tunnel/util" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | type pipingDuplex struct { 10 | downloadReaderChan <-chan interface{} // io.ReadCloser or error 11 | uploadWriter *io.PipeWriter 12 | downloadReader io.ReadCloser 13 | } 14 | 15 | func DuplexConnect(httpClient *http.Client, headers []KeyValue, uploadUrl, downloadUrl string) (*pipingDuplex, error) { 16 | return DuplexConnectWithHandlers( 17 | func(body io.Reader) (*http.Response, error) { 18 | return PipingSend(httpClient, headers, uploadUrl, body) 19 | }, 20 | func() (*http.Response, error) { 21 | return PipingGet(httpClient, headers, downloadUrl) 22 | }, 23 | ) 24 | } 25 | 26 | type postHandler = func(body io.Reader) (*http.Response, error) 27 | type getHandler = func() (*http.Response, error) 28 | 29 | func DuplexConnectWithHandlers(post postHandler, get getHandler) (*pipingDuplex, error) { 30 | uploadPr, uploadPw := io.Pipe() 31 | _, err := post(uploadPr) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | downloadReaderChan := make(chan interface{}) 37 | go func() { 38 | res, err := get() 39 | if err != nil { 40 | downloadReaderChan <- err 41 | return 42 | } 43 | downloadReaderChan <- res.Body 44 | }() 45 | 46 | return &pipingDuplex{ 47 | downloadReaderChan: downloadReaderChan, 48 | uploadWriter: uploadPw, 49 | }, nil 50 | } 51 | 52 | func (pd *pipingDuplex) Read(b []byte) (n int, err error) { 53 | if pd.downloadReaderChan != nil { 54 | // Get io.ReadCloser or error 55 | result := <-pd.downloadReaderChan 56 | // If result is error 57 | if err, ok := result.(error); ok { 58 | return 0, err 59 | } 60 | pd.downloadReader = result.(io.ReadCloser) 61 | pd.downloadReaderChan = nil 62 | } 63 | return pd.downloadReader.Read(b) 64 | } 65 | 66 | func (pd *pipingDuplex) Write(b []byte) (n int, err error) { 67 | return pd.uploadWriter.Write(b) 68 | } 69 | 70 | func (pd *pipingDuplex) Close() error { 71 | var rErr error 72 | wErr := pd.uploadWriter.Close() 73 | if pd.downloadReader != nil { 74 | rErr = pd.downloadReader.Close() 75 | } 76 | return util.CombineErrors(wErr, rErr) 77 | } 78 | -------------------------------------------------------------------------------- /openssl_aes_ctr_duplex/openssl_aes_ctr.go: -------------------------------------------------------------------------------- 1 | package openssl_aes_ctr_duplex 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "github.com/pkg/errors" 8 | "golang.org/x/crypto/pbkdf2" 9 | "hash" 10 | "io" 11 | ) 12 | 13 | type KeyAndIV struct { 14 | Key []byte 15 | Iv []byte 16 | } 17 | 18 | const ivLen = 16 19 | 20 | func DeriveKeyAndIvByPbkdf2(password []byte, salt []byte, iter int, keyLen int, h func() hash.Hash) KeyAndIV { 21 | keyAndIv := pbkdf2.Key(password, salt, iter, keyLen+ivLen, h) 22 | return KeyAndIV{ 23 | Key: keyAndIv[:keyLen], 24 | Iv: keyAndIv[keyLen:], 25 | } 26 | } 27 | 28 | func AesCtrEncryptWithPbkdf2(w io.Writer, password []byte, pbkdf2Iter int, keyLen int, h func() hash.Hash) (io.WriteCloser, error) { 29 | var salt [8]byte 30 | if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil { 31 | return nil, err 32 | } 33 | keyAndIV := DeriveKeyAndIvByPbkdf2(password, salt[:], pbkdf2Iter, keyLen, h) 34 | block, err := aes.NewCipher(keyAndIV.Key) 35 | if err != nil { 36 | return nil, err 37 | } 38 | if _, err := w.Write(append([]byte("Salted__"), salt[:]...)); err != nil { 39 | return nil, err 40 | } 41 | encryptingWriter := &cipher.StreamWriter{ 42 | S: cipher.NewCTR(block, keyAndIV.Iv), 43 | W: w, 44 | } 45 | return encryptingWriter, nil 46 | } 47 | 48 | func AesCtrDecryptWithPbkdf2(encryptedReader io.Reader, password []byte, pbkdf2Iter int, keyLen int, h func() hash.Hash) (io.Reader, error) { 49 | var eightBytes [8]byte 50 | if _, err := io.ReadFull(encryptedReader, eightBytes[:]); err != nil { 51 | return nil, err 52 | } 53 | if string(eightBytes[:]) != "Salted__" { 54 | return nil, errors.New("not start with Salted__") 55 | } 56 | // Read salt 57 | if _, err := io.ReadFull(encryptedReader, eightBytes[:]); err != nil { 58 | return nil, err 59 | } 60 | // Derive key and IV 61 | keyAndIV := DeriveKeyAndIvByPbkdf2(password, eightBytes[:], pbkdf2Iter, keyLen, h) 62 | block, err := aes.NewCipher(keyAndIV.Key) 63 | if err != nil { 64 | return nil, err 65 | } 66 | decryptedReader := &cipher.StreamReader{ 67 | S: cipher.NewCTR(block, keyAndIV.Iv), 68 | R: encryptedReader, 69 | } 70 | return decryptedReader, nil 71 | } 72 | -------------------------------------------------------------------------------- /hb_duplex/hb_duplex.go: -------------------------------------------------------------------------------- 1 | package hb_duplex 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | "github.com/pkg/errors" 7 | "io" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | const ( 13 | dataType byte = iota 14 | heartbeatType 15 | ) 16 | 17 | type hbDuplex struct { 18 | inner io.ReadWriteCloser 19 | rest uint32 20 | writeMutex *sync.Mutex 21 | } 22 | 23 | func Duplex(duplex io.ReadWriteCloser) io.ReadWriteCloser { 24 | d := &hbDuplex{inner: duplex, rest: 0, writeMutex: new(sync.Mutex)} 25 | go func() { 26 | heartbeatInterval := 30 * time.Second 27 | for { 28 | d.writeMutex.Lock() 29 | randomBytes := make([]byte, 1) 30 | io.ReadFull(rand.Reader, randomBytes) 31 | d.inner.Write([]byte{heartbeatType, randomBytes[0]}) 32 | d.writeMutex.Unlock() 33 | time.Sleep(heartbeatInterval) 34 | } 35 | }() 36 | return d 37 | } 38 | 39 | func (d *hbDuplex) Read(p []byte) (int, error) { 40 | if d.rest == 0 { 41 | b := make([]byte, 1) 42 | _, err := io.ReadFull(d.inner, b) 43 | if err != nil { 44 | return 0, err 45 | } 46 | flag := b[0] 47 | switch flag { 48 | case heartbeatType: 49 | // Discard one random byte 50 | b := make([]byte, 1) 51 | _, err := io.ReadFull(d.inner, b) 52 | if err != nil { 53 | return 0, err 54 | } 55 | return d.Read(p) 56 | case dataType: 57 | lengthBytes := make([]byte, 4) 58 | _, err = io.ReadFull(d.inner, lengthBytes) 59 | if err != nil { 60 | return 0, err 61 | } 62 | // Get length of data body 63 | d.rest = binary.BigEndian.Uint32(lengthBytes) 64 | return d.Read(p) 65 | default: 66 | return 0, errors.Errorf("unexpecrted flag: %d", flag) 67 | } 68 | } 69 | if len(p) >= int(d.rest) { 70 | p = p[0:d.rest] 71 | } 72 | n, err := d.inner.Read(p) 73 | d.rest -= uint32(n) 74 | return n, err 75 | } 76 | 77 | func (d *hbDuplex) Write(p []byte) (int, error) { 78 | length := uint32(len(p)) 79 | lengthBytes := make([]byte, 4) 80 | binary.BigEndian.PutUint32(lengthBytes, length) 81 | d.writeMutex.Lock() 82 | defer d.writeMutex.Unlock() 83 | bytes := append([]byte{dataType}, lengthBytes...) 84 | n, err := d.inner.Write(bytes) 85 | if n != len(bytes) { 86 | return n, io.ErrShortWrite 87 | } 88 | if err != nil { 89 | return 0, err 90 | } 91 | return d.inner.Write(p) 92 | } 93 | 94 | func (d *hbDuplex) Close() error { 95 | return d.inner.Close() 96 | } 97 | -------------------------------------------------------------------------------- /aes_ctr_duplex/aes_ctr_duplex.go: -------------------------------------------------------------------------------- 1 | package aes_ctr_duplex 2 | 3 | import ( 4 | "crypto" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "github.com/nwtgck/go-piping-tunnel/util" 8 | "golang.org/x/crypto/pbkdf2" 9 | "io" 10 | ) 11 | 12 | const saltLen = 64 13 | const pbkdf2Iter = 4096 14 | const keyLen = 32 15 | 16 | type aesCtrDuplex struct { 17 | encryptWriter io.WriteCloser 18 | decryptedReader io.Reader 19 | closeBaseReader func() error 20 | } 21 | 22 | func Duplex(baseWriter io.WriteCloser, baseReader io.ReadCloser, passphrase []byte) (*aesCtrDuplex, error) { 23 | // Generate salt 24 | salt1, err := util.GenerateRandomBytes(saltLen) 25 | if err != nil { 26 | return nil, err 27 | } 28 | // Send the salt 29 | if _, err := baseWriter.Write(salt1); err != nil { 30 | return nil, err 31 | } 32 | // Derive key from passphrase 33 | key1 := pbkdf2.Key(passphrase, salt1, pbkdf2Iter, keyLen, crypto.SHA512.New) 34 | block, err := aes.NewCipher(key1) 35 | if err != nil { 36 | return nil, err 37 | } 38 | // Generate IV 39 | iv1, err := util.GenerateRandomBytes(aes.BlockSize) 40 | if err != nil { 41 | return nil, err 42 | } 43 | // Send the IV 44 | if _, err := baseWriter.Write(iv1); err != nil { 45 | return nil, err 46 | } 47 | encryptWriter := &cipher.StreamWriter{ 48 | S: cipher.NewCTR(block, iv1), 49 | W: baseWriter, 50 | } 51 | block, err = aes.NewCipher(key1) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | // Read salt from peer 57 | salt2 := make([]byte, saltLen) 58 | if _, err := io.ReadFull(baseReader, salt2); err != nil { 59 | return nil, err 60 | } 61 | // Read IV from peer 62 | iv2 := make([]byte, aes.BlockSize) 63 | if _, err := io.ReadFull(baseReader, iv2); err != nil { 64 | return nil, err 65 | } 66 | // Derive key from passphrase 67 | key2 := pbkdf2.Key(passphrase, salt2, pbkdf2Iter, keyLen, crypto.SHA512.New) 68 | block2, err := aes.NewCipher(key2) 69 | if err != nil { 70 | return nil, err 71 | } 72 | decryptedReader := &cipher.StreamReader{ 73 | S: cipher.NewCTR(block2, iv2), 74 | R: baseReader, 75 | } 76 | 77 | return &aesCtrDuplex{encryptWriter: encryptWriter, decryptedReader: decryptedReader, closeBaseReader: baseReader.Close}, nil 78 | } 79 | 80 | func (d *aesCtrDuplex) Write(p []byte) (int, error) { 81 | return d.encryptWriter.Write(p) 82 | } 83 | 84 | func (d *aesCtrDuplex) Read(p []byte) (int, error) { 85 | return d.decryptedReader.Read(p) 86 | } 87 | 88 | func (d *aesCtrDuplex) Close() error { 89 | wErr := d.encryptWriter.Close() 90 | rErr := d.closeBaseReader() 91 | return util.CombineErrors(wErr, rErr) 92 | } 93 | -------------------------------------------------------------------------------- /early_piping_duplex/duplex.go: -------------------------------------------------------------------------------- 1 | // TODO: duplicate code 2 | package early_piping_duplex 3 | 4 | import ( 5 | "github.com/nwtgck/go-piping-tunnel/piping_util" 6 | "github.com/nwtgck/go-piping-tunnel/util" 7 | "github.com/pkg/errors" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | type pipingDuplex struct { 13 | uploadWriter *io.PipeWriter 14 | uploadErrChan <-chan error 15 | downloadReaderChan <-chan interface{} // io.ReadCloser or error 16 | downloadReader io.ReadCloser 17 | } 18 | 19 | func DuplexConnect(httpClient *http.Client, headers []piping_util.KeyValue, uploadUrl, downloadUrl string) (*pipingDuplex, error) { 20 | uploadPr, uploadPw := io.Pipe() 21 | uploadErrChan := make(chan error) 22 | go func() { 23 | defer close(uploadErrChan) 24 | res, err := piping_util.PipingSend(httpClient, headers, uploadUrl, uploadPr) 25 | if err != nil { 26 | uploadErrChan <- err 27 | return 28 | } 29 | if res.StatusCode != 200 { 30 | uploadErrChan <- errors.Errorf("not status 200, found: %d", res.StatusCode) 31 | return 32 | } 33 | uploadErrChan <- nil 34 | }() 35 | 36 | downloadReaderChan := make(chan interface{}) 37 | go func() { 38 | defer close(downloadReaderChan) 39 | res, err := piping_util.PipingGet(httpClient, headers, downloadUrl) 40 | if err != nil { 41 | downloadReaderChan <- err 42 | return 43 | } 44 | if res.StatusCode != 200 { 45 | downloadReaderChan <- errors.Errorf("not status 200, found: %d", res.StatusCode) 46 | } 47 | downloadReaderChan <- res.Body 48 | }() 49 | 50 | return &pipingDuplex{ 51 | uploadWriter: uploadPw, 52 | uploadErrChan: uploadErrChan, 53 | downloadReaderChan: downloadReaderChan, 54 | }, nil 55 | } 56 | 57 | func (pd *pipingDuplex) Read(b []byte) (n int, err error) { 58 | if pd.downloadReaderChan != nil { 59 | // Get io.ReadCloser or error 60 | result := <-pd.downloadReaderChan 61 | // If result is error 62 | if err, ok := result.(error); ok { 63 | return 0, err 64 | } 65 | pd.downloadReader = result.(io.ReadCloser) 66 | pd.downloadReaderChan = nil 67 | } 68 | return pd.downloadReader.Read(b) 69 | } 70 | 71 | func (pd *pipingDuplex) Write(b []byte) (n int, err error) { 72 | select { 73 | case err := <-pd.uploadErrChan: 74 | if err != nil { 75 | return 0, err 76 | } 77 | default: 78 | } 79 | return pd.uploadWriter.Write(b) 80 | } 81 | 82 | func (pd *pipingDuplex) Close() error { 83 | var wErr, rErr error 84 | if pd.uploadWriter != nil { 85 | wErr = pd.uploadWriter.Close() 86 | } 87 | if pd.downloadReader != nil { 88 | rErr = pd.downloadReader.Close() 89 | } 90 | return util.CombineErrors(wErr, rErr) 91 | } 92 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/nwtgck/go-piping-tunnel/version" 6 | "github.com/spf13/cobra" 7 | "os" 8 | ) 9 | 10 | const ( 11 | ServerUrlEnvName = "PIPING_SERVER" 12 | ) 13 | 14 | var ServerUrl string 15 | var Insecure bool 16 | var DnsServer string 17 | var showsVersion bool 18 | var ShowProgress bool 19 | var HeaderKeyValueStrs []string 20 | var HttpWriteBufSize int 21 | var HttpReadBufSize int 22 | var verboseLoggerLevel int 23 | 24 | func init() { 25 | cobra.OnInitialize() 26 | defaultServer, ok := os.LookupEnv(ServerUrlEnvName) 27 | if !ok { 28 | defaultServer = "https://ppng.io" 29 | } 30 | RootCmd.PersistentFlags().StringVarP(&ServerUrl, "server", "s", defaultServer, "Piping Server URL") 31 | RootCmd.PersistentFlags().StringVar(&DnsServer, "dns-server", "", "DNS server (e.g. 1.1.1.1:53)") 32 | // NOTE: --insecure, -k is inspired by curl 33 | RootCmd.PersistentFlags().BoolVarP(&Insecure, "insecure", "k", false, "Allow insecure server connections when using SSL") 34 | RootCmd.PersistentFlags().StringArrayVarP(&HeaderKeyValueStrs, "header", "H", []string{}, "HTTP header") 35 | RootCmd.PersistentFlags().IntVarP(&HttpWriteBufSize, "http-write-buf-size", "", 4096, "HTTP write-buffer size in bytes") 36 | RootCmd.PersistentFlags().IntVarP(&HttpReadBufSize, "http-read-buf-size", "", 4096, "HTTP read-buffer size in bytes") 37 | RootCmd.PersistentFlags().BoolVarP(&ShowProgress, "progress", "", true, "Show progress") 38 | RootCmd.Flags().BoolVarP(&showsVersion, "version", "v", false, "show version") 39 | RootCmd.PersistentFlags().IntVarP(&verboseLoggerLevel, "verbose", "", 0, "Verbose logging level") 40 | } 41 | 42 | var RootCmd = &cobra.Command{ 43 | Use: os.Args[0], 44 | Short: "piping-tunnel", 45 | Long: "Tunneling from anywhere with Piping Server", 46 | SilenceUsage: true, 47 | Example: fmt.Sprintf(` 48 | Normal: 49 | piping-tunnel server -p 22 aaa bbb 50 | piping-tunnel client -p 1022 aaa bbb 51 | 52 | Short: 53 | piping-tunnel server -p 22 mypath 54 | piping-tunnel client -p 1022 mypath 55 | 56 | Multiplexing: 57 | piping-tunnel server -p 22 --yamux aaa bbb 58 | piping-tunnel client -p 1022 --yamux aaa bbb 59 | 60 | SOCKS proxy like VPN: 61 | piping-tunnel socks --yamux aaa bbb 62 | piping-tunnel client -p 1080 --yamux aaa bbb 63 | 64 | Environment variable: 65 | $%s for default Piping Server 66 | `, ServerUrlEnvName), 67 | RunE: func(cmd *cobra.Command, args []string) error { 68 | if showsVersion { 69 | fmt.Println(version.Version) 70 | return nil 71 | } 72 | return cmd.Help() 73 | }, 74 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 75 | Vlog.Level = verboseLoggerLevel 76 | }, 77 | } 78 | -------------------------------------------------------------------------------- /io_progress/io_progress.go: -------------------------------------------------------------------------------- 1 | package io_progress 2 | 3 | import ( 4 | "fmt" 5 | "github.com/nwtgck/go-piping-tunnel/util" 6 | "io" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type IOProgress struct { 13 | CurrReadBytes uint64 14 | reader io.Reader 15 | writer io.Writer 16 | CurrWriteBytes uint64 17 | StartTime time.Time 18 | messageWriter io.Writer 19 | makeMessage func(progress *IOProgress) string 20 | maxMessageLen int 21 | lastDisplayTime time.Time 22 | finDisplayCh chan struct{} 23 | isCloseCalled bool 24 | // To process .Close() once 25 | closeMutex *sync.Mutex 26 | } 27 | 28 | func NewIOProgress(writer io.Writer, reader io.Reader, messageWriter io.Writer, makeMessage func(progress *IOProgress) string) *IOProgress { 29 | p := &IOProgress{ 30 | reader: reader, 31 | writer: writer, 32 | messageWriter: messageWriter, 33 | StartTime: time.Now(), 34 | makeMessage: makeMessage, 35 | finDisplayCh: make(chan struct{}, 1), 36 | closeMutex: new(sync.Mutex), 37 | isCloseCalled: false, 38 | } 39 | go func() { 40 | // Loop for displaying progress 41 | for len(p.finDisplayCh) == 0 { 42 | p.displayProgress() 43 | time.Sleep(1 * time.Second) 44 | } 45 | }() 46 | return p 47 | } 48 | 49 | func (progress *IOProgress) Read(p []byte) (int, error) { 50 | var n, err = progress.reader.Read(p) 51 | if err != nil { 52 | return n, err 53 | } 54 | progress.CurrReadBytes += uint64(n) 55 | return n, nil 56 | } 57 | 58 | func (progress *IOProgress) Write(p []byte) (int, error) { 59 | n, err := progress.writer.Write(p) 60 | if err != nil { 61 | return n, err 62 | } 63 | progress.CurrWriteBytes += uint64(n) 64 | return n, nil 65 | } 66 | 67 | func (progress *IOProgress) Close() error { 68 | // Lock to process once 69 | progress.closeMutex.Lock() 70 | // Unlock after processing 71 | defer progress.closeMutex.Unlock() 72 | // If already closed 73 | if progress.isCloseCalled { 74 | return nil 75 | } 76 | // Notify finish to display loop 77 | progress.finDisplayCh <- struct{}{} 78 | progress.isCloseCalled = true 79 | 80 | var rErr error 81 | var wErr error 82 | if r, ok := progress.reader.(io.ReadCloser); ok { 83 | rErr = r.Close() 84 | } 85 | if w, ok := progress.writer.(io.WriteCloser); ok { 86 | wErr = w.Close() 87 | } 88 | return util.CombineErrors(wErr, rErr) 89 | } 90 | 91 | func (progress *IOProgress) displayProgress() { 92 | // Make message 93 | message := progress.makeMessage(progress) 94 | // Clear & show message 95 | spaces := strings.Repeat(" ", progress.maxMessageLen) 96 | fmt.Fprintf(progress.messageWriter, "\r"+spaces+"\r"+message) 97 | if len(message) > progress.maxMessageLen { 98 | progress.maxMessageLen = len(message) 99 | } 100 | progress.lastDisplayTime = time.Now() 101 | } 102 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/tls" 7 | "encoding/hex" 8 | "fmt" 9 | "github.com/mattn/go-tty" 10 | "io" 11 | "math" 12 | "net" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "os/signal" 17 | "path" 18 | "syscall" 19 | "time" 20 | ) 21 | 22 | // (base: https://stackoverflow.com/a/34668130/2885946) 23 | func UrlJoin(s string, p ...string) (string, error) { 24 | u, err := url.Parse(s) 25 | if err != nil { 26 | return "", err 27 | } 28 | u.Path = path.Join(append([]string{u.Path}, p...)...) 29 | return u.String(), nil 30 | } 31 | 32 | // Generate HTTP client 33 | func CreateHttpClient(insecure bool, writeBufSize int, readBufSize int) *http.Client { 34 | // Set insecure or not 35 | tr := &http.Transport{ 36 | TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}, 37 | WriteBufferSize: writeBufSize, 38 | ReadBufferSize: readBufSize, 39 | ForceAttemptHTTP2: true, 40 | } 41 | return &http.Client{Transport: tr} 42 | } 43 | 44 | // Set default resolver for HTTP client 45 | func CreateDialContext(dnsServer string) func(ctx context.Context, network, address string) (net.Conn, error) { 46 | resolver := &net.Resolver{ 47 | PreferGo: true, 48 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 49 | d := net.Dialer{ 50 | Timeout: time.Millisecond * time.Duration(10000), 51 | } 52 | return d.DialContext(ctx, "udp", dnsServer) 53 | }, 54 | } 55 | 56 | // Resolver for HTTP 57 | return func(ctx context.Context, network, address string) (net.Conn, error) { 58 | d := net.Dialer{ 59 | Timeout: time.Millisecond * time.Duration(10000), 60 | Resolver: resolver, 61 | } 62 | return d.DialContext(ctx, network, address) 63 | } 64 | } 65 | 66 | // (base: https://github.com/schollz/progressbar/blob/9c6973820b2153b15d2e6a08d8705ec981fda59f/progressbar.go#L784-L799) 67 | func HumanizeBytes(s float64) string { 68 | if math.IsNaN(s) { 69 | return "NaN" 70 | } 71 | sizes := []string{" B", " kB", " MB", " GB", " TB", " PB", " EB"} 72 | base := 1024.0 73 | if s < 10 { 74 | return fmt.Sprintf("%2.0fB", s) 75 | } 76 | e := math.Floor(logn(s, base)) 77 | suffix := sizes[int(e)] 78 | val := math.Floor(s/math.Pow(base, e)*10+0.5) / 10 79 | f := "%.0f" 80 | if val < 10 { 81 | f = "%.1f" 82 | } 83 | 84 | return fmt.Sprintf(f, val) + suffix 85 | } 86 | 87 | // (from: https://github.com/schollz/progressbar/blob/9c6973820b2153b15d2e6a08d8705ec981fda59f/progressbar.go#L784-L799) 88 | func logn(n, b float64) float64 { 89 | return math.Log(n) / math.Log(b) 90 | } 91 | 92 | type combinedError struct { 93 | e1 error 94 | e2 error 95 | } 96 | 97 | func (e combinedError) Error() string { 98 | return fmt.Sprintf("%v and %v", e.e1, e.e2) 99 | } 100 | 101 | func CombineErrors(e1 error, e2 error) error { 102 | if e1 == nil { 103 | return e2 104 | } 105 | if e2 == nil { 106 | return e1 107 | } 108 | return &combinedError{e1: e1, e2: e2} 109 | } 110 | 111 | func InputPassphrase() (string, error) { 112 | tty, err := tty.Open() 113 | if err != nil { 114 | return "", err 115 | } 116 | defer tty.Close() 117 | quitCh := make(chan os.Signal) 118 | doneCh := make(chan struct{}) 119 | defer func() { 120 | // End this input-function normally 121 | doneCh <- struct{}{} 122 | }() 123 | go func() { 124 | signal.Notify(quitCh, syscall.SIGINT) 125 | for { 126 | select { 127 | // Signal from OS 128 | case <-quitCh: 129 | tty.Close() 130 | fmt.Println() 131 | os.Exit(0) 132 | // End this input-function normally 133 | case <-doneCh: 134 | signal.Stop(quitCh) 135 | return 136 | } 137 | } 138 | }() 139 | fmt.Fprint(tty.Output(), "Passphrase: ") 140 | passphrase, err := tty.ReadPasswordNoEcho() 141 | if err != nil { 142 | return "", err 143 | } 144 | return passphrase, nil 145 | } 146 | 147 | func GenerateRandomBytes(len int) ([]byte, error) { 148 | bytes := make([]byte, len) 149 | if _, err := io.ReadFull(rand.Reader, bytes); err != nil { 150 | return nil, err 151 | } 152 | return bytes, nil 153 | } 154 | 155 | func RandomHexString() (string, error) { 156 | // UUID: 32 hex digits + 4 dashes: https://tools.ietf.org/html/rfc4122#section-3 157 | var buf [16]byte 158 | _, err := io.ReadFull(rand.Reader, buf[:]) 159 | if err != nil { 160 | return "", err 161 | } 162 | var hexBytes [32]byte 163 | hex.Encode(hexBytes[:], buf[:]) 164 | return string(hexBytes[:]), nil 165 | } 166 | 167 | func IsTimeoutErr(err error) bool { 168 | e, ok := err.(net.Error) 169 | return ok && e.Timeout() 170 | } 171 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | 6 | ## [Unreleased] 7 | 8 | ## [0.12.0] - 2024-05-29 9 | ### Changed 10 | * Update dependencies 11 | * Warn about missing --yamux flag instead of error 12 | 13 | ## [0.11.0] - 2024-04-29 14 | ### Changed 15 | * Update dependencies 16 | * Use 4096 bytes for buffer by default 17 | 18 | ## [0.10.2] - 2022-01-15 19 | ### Changed 20 | * Update dependencies 21 | 22 | ## [0.10.1] - 2021-08-09 23 | ### Fixed 24 | * Fix progress bar not to cause "index out of range" when time.Since() returns 0 25 | 26 | ## [0.10.0] - 2021-08-09 27 | ### Added 28 | * Add OpenSSL-compatible AES-CTR encryption 29 | 30 | ### Changed 31 | * (breaking change) Rename --passphrase flag to --pass flag 32 | 33 | ## [0.9.0] - 2021-04-23 34 | ### Added 35 | * Create pmux, which is a multiplexer specialized in Piping Server 36 | * Add --host to specify target host for server host 37 | * Support SOCKS4 and SOCKS4a 38 | * Use HTTP/2 by default when the server supports 39 | * Add --verbose for logging 40 | * Add --unix-socket flags in server and client hosts 41 | 42 | ### Changed 43 | * Make --yamux attach `Content-Type: application/yamux` 44 | * (breaking change) Rename --c-to-s-buf-size to --cs-buf-size in server host 45 | * (breaking change) Rename --s-to-c-buf-size to --sc-buf-size in client host 46 | 47 | ## [0.8.0] - 2021-01-01 48 | ### Added 49 | * Add -c flag to symmetrically 50 | * Add a feature of encrypting with OpenPGP 51 | * Add a feature of encrypting with AES-CTR 52 | * Add --cipher-type flag 53 | 54 | ## [0.7.0] - 2020-12-26 55 | ### Changed 56 | * Add examples to help 57 | * Silent usage when error occurred 58 | 59 | ## [0.6.0] - 2020-12-26 60 | ### Changed 61 | * (internal) Improve performance when showing the progress bar 62 | * (internal) Improve performance when using --yamux, reducing unnecessary buffers 63 | 64 | ## [0.5.0] - 2020-12-26 65 | ### Added 66 | * Add "socks" subcommand for SOCKS5 proxy 67 | 68 | ## [0.4.2] - 2020-12-11 69 | ### Changed 70 | * No change (for release) 71 | 72 | ## [0.4.1] - 2020-12-08 73 | ### Changed 74 | * (internal) Specify buffer sizes 75 | 76 | ### Fixed 77 | * Fix hint to show socat hint when --yamux not specified 78 | 79 | ## [0.4.0] - 2020-12-06 80 | ### Added 81 | * Multiplexing with [hashicorp/yamux](https://github.com/hashicorp/yamux) and add --yamux flag 82 | 83 | ### Changed 84 | * Use ".../cs" and ".../sc" when the number of paths is one for short 85 | * Rename "$PIPING_SERVER_URL" to "$PIPING_SERVER" 86 | 87 | ## [0.3.1] - 2020-11-29 88 | ### Changed 89 | * Update dependencies 90 | 91 | ## [0.3.0] - 2020-11-04 92 | ### Added 93 | * Add --http-write-buf-size 94 | * Add --http-read-buf-size 95 | * Add --c-to-s-buf-size to client host 96 | 97 | ## [0.2.2] - 2020-10-29 98 | ### Added 99 | * Add --s-to-c-buf-size flag to client 100 | 101 | ## [0.2.1] - 2020-10-18 102 | ### Added 103 | * Add --header flag to specify HTTP header 104 | 105 | ## [0.2.0] - 2020-10-12 106 | ### Changed 107 | * Change server-host as "server" subcommand, not root command 108 | * Allow one rest argument to specify path 109 | 110 | ### Added 111 | * Create "client" subcommand 112 | * Create --progress flag to show upload/download progress (default: true) 113 | 114 | ## 0.1.0 - 2020-10-01 115 | ### Added 116 | * Initial release 117 | 118 | [Unreleased]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.12.0...HEAD 119 | [0.12.0]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.11.0...v0.12.0 120 | [0.11.0]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.10.2...v0.11.0 121 | [0.10.2]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.10.1...v0.10.2 122 | [0.10.1]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.10.0...v0.10.1 123 | [0.10.0]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.9.0...v0.10.0 124 | [0.9.0]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.8.0...v0.9.0 125 | [0.8.0]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.7.0...v0.8.0 126 | [0.7.0]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.6.0...v0.7.0 127 | [0.6.0]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.5.0...v0.6.0 128 | [0.5.0]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.4.2...v0.5.0 129 | [0.4.2]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.4.1...v0.4.2 130 | [0.4.1]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.4.0...v0.4.1 131 | [0.4.0]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.3.1...v0.4.0 132 | [0.3.1]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.3.0...v0.3.1 133 | [0.3.0]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.2.2...v0.3.0 134 | [0.2.2]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.2.1...v0.2.2 135 | [0.2.1]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.2.0...v0.2.1 136 | [0.2.0]: https://github.com/nwtgck/go-piping-tunnel/compare/v0.1.0...v0.2.0 137 | -------------------------------------------------------------------------------- /cmd/shared.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto" 5 | _ "crypto/sha1" 6 | _ "crypto/sha256" 7 | _ "crypto/sha512" 8 | "encoding/json" 9 | "fmt" 10 | "github.com/nwtgck/go-piping-tunnel/aes_ctr_duplex" 11 | "github.com/nwtgck/go-piping-tunnel/io_progress" 12 | "github.com/nwtgck/go-piping-tunnel/openpgp_duplex" 13 | "github.com/nwtgck/go-piping-tunnel/openssl_aes_ctr_duplex" 14 | "github.com/nwtgck/go-piping-tunnel/piping_util" 15 | "github.com/nwtgck/go-piping-tunnel/util" 16 | "github.com/nwtgck/go-piping-tunnel/verbose_logger" 17 | "github.com/pkg/errors" 18 | "hash" 19 | "io" 20 | "os" 21 | "time" 22 | ) 23 | 24 | const DefaultCipherType = piping_util.CipherTypeAesCtr 25 | 26 | const ( 27 | YamuxFlagLongName = "yamux" 28 | PmuxFlagLongName = "pmux" 29 | PmuxConfigFlagLongName = "pmux-config" 30 | SymmetricallyEncryptsFlagLongName = "symmetric" 31 | SymmetricallyEncryptsFlagShortName = "c" 32 | SymmetricallyEncryptPassphraseFlagLongName = "pass" 33 | CipherTypeFlagLongName = "cipher-type" 34 | Pbkdf2FlagLongName = "pbkdf2" 35 | ) 36 | 37 | const YamuxMimeType = "application/yamux" 38 | 39 | type ServerPmuxConfigJson struct { 40 | Hb bool `json:"hb"` 41 | } 42 | 43 | type ClientPmuxConfigJson struct { 44 | Hb bool `json:"hb"` 45 | } 46 | 47 | type pbkdf2ConfigJson struct { 48 | Iter int `json:"iter"` 49 | Hash string `json:"hash"` 50 | } 51 | 52 | type Pbkdf2Config struct { 53 | Iter int 54 | Hash func() hash.Hash 55 | HashNameForCommandHint string // for command hint 56 | } 57 | 58 | type OpensslAesCtrParams struct { 59 | KeyBits uint16 60 | Pbkdf2 *Pbkdf2Config 61 | } 62 | 63 | var Vlog *verbose_logger.Logger 64 | 65 | func init() { 66 | Vlog = &verbose_logger.Logger{} 67 | } 68 | 69 | func ValidateClientCipher(str string) error { 70 | switch str { 71 | case piping_util.CipherTypeAesCtr: 72 | return nil 73 | case piping_util.CipherTypeOpensslAes128Ctr: 74 | return nil 75 | case piping_util.CipherTypeOpensslAes256Ctr: 76 | return nil 77 | case piping_util.CipherTypeOpenpgp: 78 | return nil 79 | default: 80 | return errors.Errorf("invalid cipher type: %s", str) 81 | } 82 | } 83 | 84 | func validateHashFunctionName(str string) (func() hash.Hash, error) { 85 | switch str { 86 | case "sha1": 87 | return crypto.SHA1.New, nil 88 | case "sha256": 89 | return crypto.SHA256.New, nil 90 | case "sha512": 91 | return crypto.SHA512.New, nil 92 | default: 93 | return nil, errors.Errorf("unsupported hash: %s", str) 94 | } 95 | } 96 | 97 | func ParsePbkdf2(str string) (*Pbkdf2Config, error) { 98 | var configJson pbkdf2ConfigJson 99 | if json.Unmarshal([]byte(str), &configJson) != nil { 100 | return nil, errors.Errorf("invalid pbkdf2 JSON format: e.g. --%s='%s'", Pbkdf2FlagLongName, ExamplePbkdf2JsonStr()) 101 | } 102 | h, err := validateHashFunctionName(configJson.Hash) 103 | if err != nil { 104 | return nil, err 105 | } 106 | return &Pbkdf2Config{Iter: configJson.Iter, Hash: h, HashNameForCommandHint: configJson.Hash}, nil 107 | } 108 | 109 | func ParseOpensslAesCtrParams(cipherType string, pbkdf2ConfigJsonStr string) (*OpensslAesCtrParams, error) { 110 | var keyBits uint16 111 | switch cipherType { 112 | case piping_util.CipherTypeOpensslAes128Ctr: 113 | keyBits = 128 114 | case piping_util.CipherTypeOpensslAes256Ctr: 115 | keyBits = 256 116 | } 117 | switch cipherType { 118 | case piping_util.CipherTypeOpensslAes128Ctr: 119 | fallthrough 120 | case piping_util.CipherTypeOpensslAes256Ctr: 121 | pbkdf2Config, err := ParsePbkdf2(pbkdf2ConfigJsonStr) 122 | if err != nil { 123 | return nil, err 124 | } 125 | return &OpensslAesCtrParams{KeyBits: keyBits, Pbkdf2: pbkdf2Config}, nil 126 | } 127 | return nil, nil 128 | } 129 | 130 | func ExamplePbkdf2JsonStr() string { 131 | b, err := json.Marshal(&pbkdf2ConfigJson{Iter: 100000, Hash: "sha256"}) 132 | if err != nil { 133 | panic(err) 134 | } 135 | return string(b) 136 | } 137 | 138 | func GeneratePaths(args []string) (string, string, error) { 139 | var clientToServerPath string 140 | var serverToClientPath string 141 | 142 | switch len(args) { 143 | case 1: 144 | // NOTE: "cs": from client-host to server-host 145 | clientToServerPath = fmt.Sprintf("%s/cs", args[0]) 146 | // NOTE: "sc": from server-host to client-host 147 | serverToClientPath = fmt.Sprintf("%s/sc", args[0]) 148 | case 2: 149 | clientToServerPath = args[0] 150 | serverToClientPath = args[1] 151 | default: 152 | return "", "", errors.New("the number of paths should be one or two") 153 | } 154 | return clientToServerPath, serverToClientPath, nil 155 | } 156 | 157 | func MakeProgressMessage(progress *io_progress.IOProgress) string { 158 | return fmt.Sprintf( 159 | "↑ %s (%s/s) | ↓ %s (%s/s)", 160 | util.HumanizeBytes(float64(progress.CurrReadBytes)), 161 | util.HumanizeBytes(float64(progress.CurrReadBytes)/time.Since(progress.StartTime).Seconds()), 162 | util.HumanizeBytes(float64(progress.CurrWriteBytes)), 163 | util.HumanizeBytes(float64(progress.CurrWriteBytes)/time.Since(progress.StartTime).Seconds()), 164 | ) 165 | } 166 | 167 | func MakeUserInputPassphraseIfEmpty(passphrase *string) (err error) { 168 | // If the passphrase is empty 169 | if *passphrase == "" { 170 | // Get user-input passphrase 171 | *passphrase, err = util.InputPassphrase() 172 | return err 173 | } 174 | return nil 175 | } 176 | 177 | func MakeDuplexWithEncryptionAndProgressIfNeed(duplex io.ReadWriteCloser, encrypts bool, passphrase string, cipherType string, pbkdf2JsonStr string) (io.ReadWriteCloser, error) { 178 | var err error 179 | // If encryption is enabled 180 | if encrypts { 181 | var cipherName string 182 | switch cipherType { 183 | case piping_util.CipherTypeAesCtr: 184 | // Encrypt with AES-CTR 185 | duplex, err = aes_ctr_duplex.Duplex(duplex, duplex, []byte(passphrase)) 186 | cipherName = "AES-CTR" 187 | case piping_util.CipherTypeOpensslAes128Ctr: 188 | pbkdf2, err := ParsePbkdf2(pbkdf2JsonStr) 189 | if err != nil { 190 | return nil, err 191 | } 192 | duplex, err = openssl_aes_ctr_duplex.Duplex(duplex, duplex, []byte(passphrase), pbkdf2.Iter, 128/8, pbkdf2.Hash) 193 | cipherName = "OpenSSL-AES-128-CTR-compatible" 194 | case piping_util.CipherTypeOpensslAes256Ctr: 195 | pbkdf2, err := ParsePbkdf2(pbkdf2JsonStr) 196 | if err != nil { 197 | return nil, err 198 | } 199 | duplex, err = openssl_aes_ctr_duplex.Duplex(duplex, duplex, []byte(passphrase), pbkdf2.Iter, 256/8, pbkdf2.Hash) 200 | cipherName = "OpenSSL-AES-256-CTR-compatible" 201 | case piping_util.CipherTypeOpenpgp: 202 | duplex, err = openpgp_duplex.SymmetricallyEncryptDuplexWithOpenPGP(duplex, duplex, []byte(passphrase)) 203 | cipherName = "OpenPGP" 204 | default: 205 | return nil, errors.Errorf("unexpected cipher type: %s", cipherType) 206 | } 207 | if err != nil { 208 | return nil, err 209 | } 210 | fmt.Printf("[INFO] End-to-end encryption with %s\n", cipherName) 211 | } 212 | if ShowProgress { 213 | duplex = io_progress.NewIOProgress(duplex, duplex, os.Stderr, MakeProgressMessage) 214 | } 215 | return duplex, nil 216 | } 217 | 218 | func HeadersWithYamux(headers []piping_util.KeyValue) []piping_util.KeyValue { 219 | return append(headers, piping_util.KeyValue{Key: "Content-Type", Value: YamuxMimeType}) 220 | } 221 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # piping-tunnel 2 | ![CI](https://github.com/nwtgck/go-piping-tunnel/workflows/CI/badge.svg) 3 | 4 | Tunneling over HTTP with [Piping Server](https://github.com/nwtgck/piping-server) 5 | 6 | 7 | ## Install for Windows 8 | [Download](https://github.com/nwtgck/go-piping-tunnel/releases/download/v0.10.1/piping-tunnel-0.10.1-windows-amd64.zip) 9 | 10 | ## Install for macOS 11 | ```bash 12 | brew install nwtgck/piping-tunnel/piping-tunnel 13 | ``` 14 | 15 | ## Install for Ubuntu 16 | ```bash 17 | wget https://github.com/nwtgck/go-piping-tunnel/releases/download/v0.10.1/piping-tunnel-0.10.1-linux-amd64.deb 18 | sudo dpkg -i piping-tunnel-0.10.1-linux-amd64.deb 19 | ``` 20 | 21 | Get more executables in the [releases](https://github.com/nwtgck/go-piping-tunnel/releases). 22 | 23 | ## Help 24 | 25 | You can use `$PIPING_SERVER` env to set default Piping Server. 26 | 27 | ```txt 28 | Tunneling from anywhere with Piping Server 29 | 30 | Usage: 31 | piping-tunnel [flags] 32 | piping-tunnel [command] 33 | 34 | Examples: 35 | 36 | Normal: 37 | piping-tunnel server -p 22 aaa bbb 38 | piping-tunnel client -p 1022 aaa bbb 39 | 40 | Short: 41 | piping-tunnel server -p 22 mypath 42 | piping-tunnel client -p 1022 mypath 43 | 44 | Multiplexing: 45 | piping-tunnel server -p 22 --yamux aaa bbb 46 | piping-tunnel client -p 1022 --yamux aaa bbb 47 | 48 | SOCKS proxy like VPN: 49 | piping-tunnel socks --yamux aaa bbb 50 | piping-tunnel client -p 1080 --yamux aaa bbb 51 | 52 | Environment variable: 53 | $PIPING_SERVER for default Piping Server 54 | 55 | 56 | Available Commands: 57 | client Run client-host 58 | completion Generate the autocompletion script for the specified shell 59 | help Help about any command 60 | server Run server-host 61 | socks Run SOCKS server 62 | 63 | Flags: 64 | --dns-server string DNS server (e.g. 1.1.1.1:53) 65 | -H, --header stringArray HTTP header 66 | -h, --help help for piping-tunnel 67 | --http-read-buf-size int HTTP read-buffer size in bytes (default 4096) 68 | --http-write-buf-size int HTTP write-buffer size in bytes (default 4096) 69 | -k, --insecure Allow insecure server connections when using SSL 70 | --progress Show progress (default true) 71 | -s, --server string Piping Server URL (default "https://ppng.io") 72 | --verbose int Verbose logging level 73 | -v, --version show version 74 | 75 | Use "piping-tunnel [command] --help" for more information about a command. 76 | ``` 77 | 78 | The following help is for server-host. 79 | ``` 80 | Run server-host 81 | 82 | Usage: 83 | piping-tunnel server [flags] 84 | 85 | Flags: 86 | --cipher-type string Cipher type: aes-ctr, openssl-aes-128-ctr, openssl-aes-256-ctr, openpgp (default "aes-ctr") 87 | --cs-buf-size uint Buffer size of client-to-server in bytes (default 4096) 88 | -h, --help help for server 89 | --host string Target host (default "localhost") 90 | --pass string Passphrase for encryption 91 | --pbkdf2 string e.g. {"iter":100000,"hash":"sha256"} 92 | --pmux Multiplex connection by pmux (experimental) 93 | --pmux-config string pmux config in JSON (experimental) (default "{\"hb\": true}") 94 | -p, --port int TCP port of server host 95 | -c, --symmetric Encrypt symmetrically 96 | --unix-socket string Unix socket of server host 97 | --yamux Multiplex connection by hashicorp/yamux 98 | 99 | Global Flags: 100 | --dns-server string DNS server (e.g. 1.1.1.1:53) 101 | -H, --header stringArray HTTP header 102 | --http-read-buf-size int HTTP read-buffer size in bytes (default 4096) 103 | --http-write-buf-size int HTTP write-buffer size in bytes (default 4096) 104 | -k, --insecure Allow insecure server connections when using SSL 105 | --progress Show progress (default true) 106 | -s, --server string Piping Server URL (default "https://ppng.io") 107 | --verbose int Verbose logging level 108 | ``` 109 | 110 | The following help is for client-host. 111 | ``` 112 | Run client-host 113 | 114 | Usage: 115 | piping-tunnel client [flags] 116 | 117 | Flags: 118 | --cipher-type string Cipher type: aes-ctr, openssl-aes-128-ctr, openssl-aes-256-ctr, openpgp (default "aes-ctr") 119 | -h, --help help for client 120 | --pass string Passphrase for encryption 121 | --pbkdf2 string e.g. {"iter":100000,"hash":"sha256"} 122 | --pmux Multiplex connection by pmux (experimental) 123 | --pmux-config string pmux config in JSON (experimental) (default "{\"hb\": true}") 124 | -p, --port int TCP port of client host 125 | --sc-buf-size uint Buffer size of server-to-client in bytes (default 4096) 126 | -c, --symmetric Encrypt symmetrically 127 | --unix-socket string Unix socket of client host 128 | --yamux Multiplex connection by hashicorp/yamux 129 | 130 | Global Flags: 131 | --dns-server string DNS server (e.g. 1.1.1.1:53) 132 | -H, --header stringArray HTTP header 133 | --http-read-buf-size int HTTP read-buffer size in bytes (default 4096) 134 | --http-write-buf-size int HTTP write-buffer size in bytes (default 4096) 135 | -k, --insecure Allow insecure server connections when using SSL 136 | --progress Show progress (default true) 137 | -s, --server string Piping Server URL (default "https://ppng.io") 138 | --verbose int Verbose logging level 139 | ``` 140 | 141 | The following help is for SOCKS proxy. 142 | 143 | ``` 144 | Run SOCKS server 145 | 146 | Usage: 147 | piping-tunnel socks [flags] 148 | 149 | Flags: 150 | --cipher-type string Cipher type: aes-ctr, openssl-aes-128-ctr, openssl-aes-256-ctr, openpgp (default "aes-ctr") 151 | -h, --help help for socks 152 | --pass string Passphrase for encryption 153 | --pbkdf2 string e.g. {"iter":100000,"hash":"sha256"} 154 | --pmux Multiplex connection by pmux (experimental) 155 | --pmux-config string pmux config in JSON (experimental) (default "{\"hb\": true}") 156 | -c, --symmetric Encrypt symmetrically 157 | --yamux Multiplex connection by hashicorp/yamux 158 | 159 | Global Flags: 160 | --dns-server string DNS server (e.g. 1.1.1.1:53) 161 | -H, --header stringArray HTTP header 162 | --http-read-buf-size int HTTP read-buffer size in bytes (default 4096) 163 | --http-write-buf-size int HTTP write-buffer size in bytes (default 4096) 164 | -k, --insecure Allow insecure server connections when using SSL 165 | --progress Show progress (default true) 166 | -s, --server string Piping Server URL (default "https://ppng.io") 167 | --verbose int Verbose logging level 168 | ``` 169 | 170 | ## References 171 | The idea of tunneling over Piping Server was proposed by [@Cryolite](https://github.com/Cryolite). Thanks! 172 | - (Japanese) 173 | 174 | ## Related work 175 | - [portwarp](https://github.com/essa/portwarp) 176 | -------------------------------------------------------------------------------- /cmd/socks/socks.go: -------------------------------------------------------------------------------- 1 | package socks 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/hashicorp/yamux" 7 | "github.com/nwtgck/go-piping-tunnel/cmd" 8 | "github.com/nwtgck/go-piping-tunnel/piping_util" 9 | "github.com/nwtgck/go-piping-tunnel/pmux" 10 | "github.com/nwtgck/go-piping-tunnel/util" 11 | "github.com/nwtgck/go-socks" 12 | "github.com/pkg/errors" 13 | "github.com/spf13/cobra" 14 | "io" 15 | "net/http" 16 | ) 17 | 18 | var flag struct { 19 | yamux bool 20 | pmux bool 21 | pmuxConfig string 22 | symmetricallyEncrypts bool 23 | symmetricallyEncryptPassphrase string 24 | cipherType string 25 | pbkdf2JsonString string 26 | } 27 | 28 | func init() { 29 | cmd.RootCmd.AddCommand(socksCmd) 30 | socksCmd.Flags().BoolVarP(&flag.yamux, "yamux", "", false, "Multiplex connection by hashicorp/yamux") 31 | socksCmd.Flags().BoolVarP(&flag.pmux, cmd.PmuxFlagLongName, "", false, "Multiplex connection by pmux (experimental)") 32 | socksCmd.Flags().StringVarP(&flag.pmuxConfig, cmd.PmuxConfigFlagLongName, "", `{"hb": true}`, "pmux config in JSON (experimental)") 33 | socksCmd.Flags().BoolVarP(&flag.symmetricallyEncrypts, cmd.SymmetricallyEncryptsFlagLongName, cmd.SymmetricallyEncryptsFlagShortName, false, "Encrypt symmetrically") 34 | socksCmd.Flags().StringVarP(&flag.symmetricallyEncryptPassphrase, cmd.SymmetricallyEncryptPassphraseFlagLongName, "", "", "Passphrase for encryption") 35 | socksCmd.Flags().StringVarP(&flag.cipherType, cmd.CipherTypeFlagLongName, "", cmd.DefaultCipherType, fmt.Sprintf("Cipher type: %s, %s, %s, %s ", piping_util.CipherTypeAesCtr, piping_util.CipherTypeOpensslAes128Ctr, piping_util.CipherTypeOpensslAes256Ctr, piping_util.CipherTypeOpenpgp)) 36 | // NOTE: default value of --pbkdf2 should be empty to detect key derive derivation from multiple algorithms in the future. 37 | socksCmd.Flags().StringVarP(&flag.pbkdf2JsonString, cmd.Pbkdf2FlagLongName, "", "", fmt.Sprintf("e.g. %s", cmd.ExamplePbkdf2JsonStr())) 38 | } 39 | 40 | var socksCmd = &cobra.Command{ 41 | Use: "socks", 42 | Short: "Run SOCKS server", 43 | RunE: func(_ *cobra.Command, args []string) error { 44 | // Validate cipher-type 45 | if flag.symmetricallyEncrypts { 46 | if err := cmd.ValidateClientCipher(flag.cipherType); err != nil { 47 | return err 48 | } 49 | } 50 | clientToServerPath, serverToClientPath, err := cmd.GeneratePaths(args) 51 | if err != nil { 52 | return err 53 | } 54 | headers, err := piping_util.ParseKeyValueStrings(cmd.HeaderKeyValueStrs) 55 | if err != nil { 56 | return err 57 | } 58 | httpClient := util.CreateHttpClient(cmd.Insecure, cmd.HttpWriteBufSize, cmd.HttpReadBufSize) 59 | if cmd.DnsServer != "" { 60 | // Set DNS resolver 61 | httpClient.Transport.(*http.Transport).DialContext = util.CreateDialContext(cmd.DnsServer) 62 | } 63 | serverToClientUrl, err := util.UrlJoin(cmd.ServerUrl, serverToClientPath) 64 | if err != nil { 65 | return err 66 | } 67 | clientToServerUrl, err := util.UrlJoin(cmd.ServerUrl, clientToServerPath) 68 | if err != nil { 69 | return err 70 | } 71 | // Print hint 72 | socksPrintHintForClientHost(clientToServerPath, serverToClientPath) 73 | // Make user input passphrase if it is empty 74 | if flag.symmetricallyEncrypts { 75 | err = cmd.MakeUserInputPassphraseIfEmpty(&flag.symmetricallyEncryptPassphrase) 76 | if err != nil { 77 | return err 78 | } 79 | } 80 | 81 | // If not using multiplexer 82 | if !flag.yamux && !flag.pmux { 83 | return errors.Errorf("--%s or --%s must be specified", cmd.YamuxFlagLongName, cmd.PmuxFlagLongName) 84 | } 85 | 86 | socksConf := &socks.Config{} 87 | socksServer, err := socks.New(socksConf) 88 | 89 | // If yamux is enabled 90 | if flag.yamux { 91 | fmt.Println("[INFO] Multiplexing with hashicorp/yamux") 92 | return socksHandleWithYamux(socksServer, httpClient, headers, clientToServerUrl, serverToClientUrl) 93 | } 94 | 95 | // If pmux is enabled 96 | fmt.Println("[INFO] Multiplexing with pmux") 97 | return socksHandleWithPmux(socksServer, httpClient, headers, clientToServerUrl, serverToClientUrl) 98 | }, 99 | } 100 | 101 | // NOTE: multiplexing should be enabled, so there is no socat-curl hint 102 | func socksPrintHintForClientHost(clientToServerPath string, serverToClientPath string) { 103 | flags := "" 104 | if flag.symmetricallyEncrypts { 105 | flags += fmt.Sprintf("-%s ", cmd.SymmetricallyEncryptsFlagShortName) 106 | flags += fmt.Sprintf("--%s=%s ", cmd.CipherTypeFlagLongName, flag.cipherType) 107 | switch flag.cipherType { 108 | case piping_util.CipherTypeOpensslAes128Ctr: 109 | fallthrough 110 | case piping_util.CipherTypeOpensslAes256Ctr: 111 | flags += fmt.Sprintf("--%s='%s' ", cmd.Pbkdf2FlagLongName, flag.pbkdf2JsonString) 112 | } 113 | } 114 | if flag.yamux { 115 | flags += fmt.Sprintf("--%s ", cmd.YamuxFlagLongName) 116 | } 117 | if flag.pmux { 118 | flags += fmt.Sprintf("--%s ", cmd.PmuxFlagLongName) 119 | } 120 | fmt.Println("[INFO] Hint: Client host (piping-tunnel)") 121 | fmt.Printf( 122 | " piping-tunnel -s %s client -p 1080 %s%s %s\n", 123 | cmd.ServerUrl, 124 | flags, 125 | clientToServerPath, 126 | serverToClientPath, 127 | ) 128 | } 129 | 130 | func socksHandleWithYamux(socksServer *socks.Server, httpClient *http.Client, headers []piping_util.KeyValue, clientToServerUrl string, serverToClientUrl string) error { 131 | var duplex io.ReadWriteCloser 132 | duplex, err := piping_util.DuplexConnectWithHandlers( 133 | func(body io.Reader) (*http.Response, error) { 134 | return piping_util.PipingSend(httpClient, cmd.HeadersWithYamux(headers), serverToClientUrl, body) 135 | }, 136 | func() (*http.Response, error) { 137 | res, err := piping_util.PipingGet(httpClient, headers, clientToServerUrl) 138 | if err != nil { 139 | return nil, err 140 | } 141 | contentType := res.Header.Get("Content-Type") 142 | // NOTE: application/octet-stream is for compatibility 143 | if contentType != cmd.YamuxMimeType && contentType != "application/octet-stream" { 144 | return nil, errors.Errorf("invalid content-type: %s", contentType) 145 | } 146 | return res, nil 147 | }, 148 | ) 149 | duplex, err = cmd.MakeDuplexWithEncryptionAndProgressIfNeed(duplex, flag.symmetricallyEncrypts, flag.symmetricallyEncryptPassphrase, flag.cipherType, flag.pbkdf2JsonString) 150 | if err != nil { 151 | return err 152 | } 153 | yamuxSession, err := yamux.Server(duplex, nil) 154 | if err != nil { 155 | return err 156 | } 157 | for { 158 | yamuxStream, err := yamuxSession.Accept() 159 | if err != nil { 160 | return err 161 | } 162 | go socksServer.ServeConn(yamuxStream) 163 | } 164 | } 165 | 166 | func socksHandleWithPmux(socksServer *socks.Server, httpClient *http.Client, headers []piping_util.KeyValue, clientToServerUrl string, serverToClientUrl string) error { 167 | var config cmd.ServerPmuxConfigJson 168 | if json.Unmarshal([]byte(flag.pmuxConfig), &config) != nil { 169 | return errors.Errorf("invalid pmux config format") 170 | } 171 | pmuxServer := pmux.Server(httpClient, headers, serverToClientUrl, clientToServerUrl, config.Hb, flag.symmetricallyEncrypts, flag.symmetricallyEncryptPassphrase, flag.cipherType) 172 | for { 173 | stream, err := pmuxServer.Accept() 174 | if err != nil { 175 | return err 176 | } 177 | go func() { 178 | err := socksServer.ServeConn(util.NewDuplexConn(stream)) 179 | if err != nil { 180 | cmd.Vlog.Log( 181 | fmt.Sprintf("error(serve conn): %v", errors.WithStack(err)), 182 | fmt.Sprintf("error(serve conn): %+v", errors.WithStack(err)), 183 | ) 184 | } 185 | }() 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= 4 | github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= 5 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 6 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 7 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 8 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 9 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 10 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 11 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 12 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 13 | github.com/mattn/go-tty v0.0.5 h1:s09uXI7yDbXzzTTfw3zonKFzwGkyYlgU3OMjqA0ddz4= 14 | github.com/mattn/go-tty v0.0.5/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28= 15 | github.com/nwtgck/go-socks v0.1.0 h1:tyiyFHhvcRez3UfQ8HVOv0+aKVat5zt/RT7SG8aaG0Q= 16 | github.com/nwtgck/go-socks v0.1.0/go.mod h1:TJyaIsmzydZE5qNvglbCrdCWigpDITLqY7d8ve6TXnE= 17 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 18 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 19 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 20 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 21 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 22 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 23 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 24 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 26 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 27 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 28 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 29 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 30 | golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= 31 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 32 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 33 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 34 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 35 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 36 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 37 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 38 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 39 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 40 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 41 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 42 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 43 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 44 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 45 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 46 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 47 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 48 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 50 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 51 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 52 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 53 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 65 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 66 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 67 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 68 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 69 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 70 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 71 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 72 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 73 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 74 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 75 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 76 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 77 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 78 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 79 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 80 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 81 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 82 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 83 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 84 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 85 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 86 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 87 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 88 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 89 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 90 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 91 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 92 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 93 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 95 | -------------------------------------------------------------------------------- /pmux/pmux.go: -------------------------------------------------------------------------------- 1 | // TODO: should send fin to notify finish 2 | package pmux 3 | 4 | import ( 5 | "bytes" 6 | "context" 7 | "encoding/binary" 8 | "encoding/json" 9 | "fmt" 10 | "github.com/nwtgck/go-piping-tunnel/aes_ctr_duplex" 11 | "github.com/nwtgck/go-piping-tunnel/backoff" 12 | "github.com/nwtgck/go-piping-tunnel/early_piping_duplex" 13 | "github.com/nwtgck/go-piping-tunnel/hb_duplex" 14 | "github.com/nwtgck/go-piping-tunnel/openpgp_duplex" 15 | "github.com/nwtgck/go-piping-tunnel/piping_util" 16 | "github.com/nwtgck/go-piping-tunnel/util" 17 | "github.com/pkg/errors" 18 | "io" 19 | "net/http" 20 | "os" 21 | "time" 22 | ) 23 | 24 | type server struct { 25 | httpClient *http.Client 26 | headers []piping_util.KeyValue 27 | baseUploadUrl string 28 | baseDownloadUrl string 29 | enableHb bool 30 | encrypts bool 31 | passphrase string 32 | cipherType string // NOTE: encryption in pmux can be updated in the different way in the future such as negotiating algorithm 33 | } 34 | 35 | type client struct { 36 | httpClient *http.Client 37 | headers []piping_util.KeyValue 38 | baseUploadUrl string 39 | baseDownloadUrl string 40 | enableHb bool 41 | encrypts bool 42 | passphrase string 43 | cipherType string 44 | } 45 | 46 | type serverConfigJson struct { 47 | Hb bool `json:"hb"` 48 | } 49 | 50 | type syncJson struct { 51 | SubPath string `json:"sub_path"` 52 | } 53 | 54 | const pmuxVersion uint32 = 1 55 | const pmuxMimeType = "application/pmux" 56 | const httpTimeout = 50 * time.Second 57 | 58 | var pmuxVersionBytes [4]byte 59 | var IncompatiblePmuxVersion = errors.Errorf("incompatible pmux version, expected %d", pmuxVersion) 60 | var NonPmuxMimeTypeError = errors.Errorf("invalid content-type, expected %s", pmuxMimeType) 61 | var IncompatibleServerConfigError = errors.Errorf("imcompatible server config") 62 | var DifferentHbSettingError = errors.Errorf("different hb setting from server's") 63 | 64 | func init() { 65 | binary.BigEndian.PutUint32(pmuxVersionBytes[:], pmuxVersion) 66 | } 67 | 68 | func headersWithPmux(headers []piping_util.KeyValue) []piping_util.KeyValue { 69 | return append(headers, piping_util.KeyValue{Key: "Content-Type", Value: pmuxMimeType}) 70 | } 71 | 72 | func Server(httpClient *http.Client, headers []piping_util.KeyValue, baseUploadUrl string, baseDownloadUrl string, enableHb bool, encrypts bool, passphrase string, cipherType string) *server { 73 | server := &server{ 74 | httpClient: httpClient, 75 | headers: headers, 76 | baseUploadUrl: baseUploadUrl, 77 | baseDownloadUrl: baseDownloadUrl, 78 | enableHb: enableHb, 79 | encrypts: encrypts, 80 | passphrase: passphrase, 81 | cipherType: cipherType, 82 | } 83 | go server.sendVersionAndConfigLoop() 84 | return server 85 | } 86 | 87 | type getSubPathStatusError struct { 88 | statusCode int 89 | } 90 | 91 | func (e *getSubPathStatusError) Error() string { 92 | return fmt.Sprintf("not status 200, found: %d", e.statusCode) 93 | } 94 | 95 | func (s *server) sendVersionAndConfigLoop() { 96 | b := backoff.NewExponentialBackoff() 97 | for { 98 | ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) 99 | defer cancel() 100 | // NOTE: In the future, config scheme can change more efficient format than JSON 101 | configJsonBytes, err := json.Marshal(serverConfigJson{Hb: s.enableHb}) 102 | if err != nil { 103 | // backoff 104 | time.Sleep(b.NextDuration()) 105 | continue 106 | } 107 | postRes, err := piping_util.PipingSendWithContext(ctx, s.httpClient, headersWithPmux(s.headers), s.baseUploadUrl, bytes.NewReader(append(pmuxVersionBytes[:], configJsonBytes...))) 108 | if postRes.StatusCode != 200 { 109 | // backoff 110 | time.Sleep(b.NextDuration()) 111 | continue 112 | } 113 | // If timeout 114 | if util.IsTimeoutErr(err) { 115 | // reset backoff 116 | b.Reset() 117 | // No backoff 118 | continue 119 | } 120 | if err != nil { 121 | // backoff 122 | time.Sleep(b.NextDuration()) 123 | continue 124 | } 125 | _, err = io.Copy(io.Discard, postRes.Body) 126 | if err != nil { 127 | // backoff 128 | time.Sleep(b.NextDuration()) 129 | continue 130 | } 131 | } 132 | } 133 | 134 | func (s *server) getSubPath() (string, error) { 135 | ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) 136 | defer cancel() 137 | getRes, err := piping_util.PipingGetWithContext(ctx, s.httpClient, s.headers, s.baseDownloadUrl) 138 | if err != nil { 139 | return "", err 140 | } 141 | if getRes.StatusCode != 200 { 142 | return "", &getSubPathStatusError{statusCode: getRes.StatusCode} 143 | } 144 | resBytes, err := io.ReadAll(getRes.Body) 145 | if err != nil { 146 | return "", err 147 | } 148 | var sync syncJson 149 | err = json.Unmarshal(resBytes, &sync) 150 | if err != nil { 151 | return "", err 152 | } 153 | return sync.SubPath, nil 154 | } 155 | 156 | func (s *server) Accept() (io.ReadWriteCloser, error) { 157 | b := backoff.NewExponentialBackoff() 158 | var subPath string 159 | for { 160 | var err error 161 | subPath, err = s.getSubPath() 162 | if err == nil { 163 | break 164 | } 165 | // If timeout 166 | if util.IsTimeoutErr(err) { 167 | // reset backoff 168 | b.Reset() 169 | // No backoff 170 | continue 171 | } 172 | // backoff 173 | time.Sleep(b.NextDuration()) 174 | } 175 | uploadUrl, err := util.UrlJoin(s.baseUploadUrl, subPath) 176 | if err != nil { 177 | return nil, err 178 | } 179 | downloadUrl, err := util.UrlJoin(s.baseDownloadUrl, subPath) 180 | if err != nil { 181 | return nil, err 182 | } 183 | var duplex io.ReadWriteCloser 184 | duplex, err = early_piping_duplex.DuplexConnect(s.httpClient, s.headers, uploadUrl, downloadUrl) 185 | if err != nil { 186 | return nil, err 187 | } 188 | if s.enableHb { 189 | duplex = hb_duplex.Duplex(duplex) 190 | } 191 | if s.encrypts { 192 | switch s.cipherType { 193 | case piping_util.CipherTypeAesCtr: 194 | // Encrypt with AES-CTR 195 | duplex, err = aes_ctr_duplex.Duplex(duplex, duplex, []byte(s.passphrase)) 196 | case piping_util.CipherTypeOpenpgp: 197 | duplex, err = openpgp_duplex.SymmetricallyEncryptDuplexWithOpenPGP(duplex, duplex, []byte(s.passphrase)) 198 | // NOTE: pmux does not support openssl-compatible encryption 199 | default: 200 | return nil, errors.Errorf("unexpected cipher type: %s", s.cipherType) 201 | } 202 | } 203 | return duplex, err 204 | } 205 | 206 | func Client(httpClient *http.Client, headers []piping_util.KeyValue, baseUploadUrl string, baseDownloadUrl string, enableHb bool, encrypts bool, passphrase string, cipherType string) (*client, error) { 207 | client := &client{ 208 | httpClient: httpClient, 209 | headers: headers, 210 | baseUploadUrl: baseUploadUrl, 211 | baseDownloadUrl: baseDownloadUrl, 212 | enableHb: enableHb, 213 | encrypts: encrypts, 214 | passphrase: passphrase, 215 | cipherType: cipherType, 216 | } 217 | return client, client.checkServerVersionAndConfig() 218 | } 219 | 220 | func (c *client) checkServerVersionAndConfig() error { 221 | b := backoff.NewExponentialBackoff() 222 | for { 223 | ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) 224 | defer cancel() 225 | postRes, err := piping_util.PipingGetWithContext(ctx, c.httpClient, c.headers, c.baseDownloadUrl) 226 | // If timeout 227 | if util.IsTimeoutErr(err) { 228 | // reset backoff 229 | b.Reset() 230 | // No backoff 231 | continue 232 | } 233 | if err != nil { 234 | // backoff 235 | time.Sleep(b.NextDuration()) 236 | continue 237 | } 238 | if postRes.StatusCode != 200 { 239 | // backoff 240 | time.Sleep(b.NextDuration()) 241 | continue 242 | } 243 | if postRes.Header.Get("Content-Type") != pmuxMimeType { 244 | return NonPmuxMimeTypeError 245 | } 246 | versionBytes := make([]byte, 4) 247 | _, err = io.ReadFull(postRes.Body, versionBytes) 248 | if err != nil { 249 | // backoff 250 | time.Sleep(b.NextDuration()) 251 | continue 252 | } 253 | serverVersion := binary.BigEndian.Uint32(versionBytes) 254 | if serverVersion != pmuxVersion { 255 | return IncompatiblePmuxVersion 256 | } 257 | serverConfigJsonBytes, err := io.ReadAll(postRes.Body) 258 | if err != nil { 259 | // backoff 260 | time.Sleep(b.NextDuration()) 261 | continue 262 | } 263 | var serverConfig serverConfigJson 264 | if json.Unmarshal(serverConfigJsonBytes, &serverConfig) != nil { 265 | return IncompatibleServerConfigError 266 | } 267 | if serverConfig.Hb != c.enableHb { 268 | return DifferentHbSettingError 269 | } 270 | return nil 271 | } 272 | } 273 | 274 | func (c *client) sendSubPath() (string, error) { 275 | subPath, err := util.RandomHexString() 276 | if err != nil { 277 | return "", err 278 | } 279 | sync := syncJson{SubPath: subPath} 280 | jsonBytes, err := json.Marshal(sync) 281 | if err != nil { 282 | return "", err 283 | } 284 | res, err := piping_util.PipingSend(c.httpClient, c.headers, c.baseUploadUrl, bytes.NewReader(jsonBytes)) 285 | if err != nil { 286 | return "", err 287 | } 288 | if res.StatusCode != 200 { 289 | return "", errors.Errorf("not status 200, found: %d", res.StatusCode) 290 | } 291 | _, err = io.Copy(io.Discard, res.Body) 292 | return subPath, err 293 | } 294 | 295 | func (c *client) Open() (io.ReadWriteCloser, error) { 296 | b := backoff.NewExponentialBackoff() 297 | var subPath string 298 | for { 299 | var err error 300 | subPath, err = c.sendSubPath() 301 | if err == nil { 302 | break 303 | } 304 | // If timeout 305 | if util.IsTimeoutErr(err) { 306 | b.Reset() 307 | continue 308 | } 309 | fmt.Fprintln(os.Stderr, "get sync error", err) 310 | time.Sleep(b.NextDuration()) 311 | } 312 | uploadUrl, err := util.UrlJoin(c.baseUploadUrl, subPath) 313 | if err != nil { 314 | return nil, err 315 | } 316 | downloadUrl, err := util.UrlJoin(c.baseDownloadUrl, subPath) 317 | if err != nil { 318 | return nil, err 319 | } 320 | var duplex io.ReadWriteCloser 321 | duplex, err = early_piping_duplex.DuplexConnect(c.httpClient, c.headers, uploadUrl, downloadUrl) 322 | if err != nil { 323 | return nil, err 324 | } 325 | if c.enableHb { 326 | duplex = hb_duplex.Duplex(duplex) 327 | } 328 | if c.encrypts { 329 | switch c.cipherType { 330 | case piping_util.CipherTypeAesCtr: 331 | // Encrypt with AES-CTR 332 | duplex, err = aes_ctr_duplex.Duplex(duplex, duplex, []byte(c.passphrase)) 333 | case piping_util.CipherTypeOpenpgp: 334 | duplex, err = openpgp_duplex.SymmetricallyEncryptDuplexWithOpenPGP(duplex, duplex, []byte(c.passphrase)) 335 | // NOTE: pmux does not support openssl-compatible encryption 336 | default: 337 | return nil, errors.Errorf("unexpected cipher type: %s", c.cipherType) 338 | } 339 | } 340 | return duplex, err 341 | } 342 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build_multi_platform: 7 | runs-on: ubuntu-22.04 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Set up Go 1.x 11 | uses: actions/setup-go@v5 12 | with: 13 | go-version: '1.20' 14 | - name: Build for multi-platform 15 | run: | 16 | set -xeu 17 | DIST=dist 18 | mkdir $DIST 19 | # (from: https://www.digitalocean.com/community/tutorials/how-to-build-go-executables-for-multiple-platforms-on-ubuntu-16-04) 20 | platforms=("linux/amd64" "darwin/amd64" "windows/amd64" "linux/arm") 21 | for platform in "${platforms[@]}" 22 | do 23 | platform_split=(${platform//\// }) 24 | export GOOS=${platform_split[0]} 25 | export GOARCH=${platform_split[1]} 26 | [ $GOOS = "windows" ] && EXTENSION='.exe' || EXTENSION='' 27 | BUILD_PATH=piping-tunnel-$GOOS-$GOARCH 28 | mkdir $BUILD_PATH 29 | # Build 30 | CGO_ENABLED=0 go build -o "${BUILD_PATH}/piping-tunnel${EXTENSION}" main/main.go 31 | done 32 | operational_test: 33 | runs-on: ubuntu-22.04 34 | defaults: 35 | run: 36 | shell: bash 37 | steps: 38 | - name: Build SSH server Dockerfile 39 | run: | 40 | docker build -t ssh-server - <<'EOS' 41 | FROM ubuntu:20.04 42 | RUN apt update 43 | RUN apt install -y openssh-server 44 | RUN mkdir /var/run/sshd 45 | 46 | # (base(ja): https://qiita.com/FGtatsuro/items/4893dfb138f70d972904) 47 | RUN useradd -m guest 48 | RUN passwd -d guest 49 | RUN sed -ri 's/^#?PermitEmptyPasswords\s+.*/PermitEmptyPasswords yes/' /etc/ssh/sshd_config 50 | RUN sed -ri 's/^#?UsePAM\s+.*/UsePAM no/' /etc/ssh/sshd_config 51 | 52 | # SSH login fix. Otherwise user is kicked off after login 53 | RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd 54 | 55 | ENV NOTVISIBLE "in users profile" 56 | RUN echo "export VISIBLE=now" >> /etc/profile 57 | ENTRYPOINT [ "/usr/sbin/sshd", "-D" ] 58 | EOS 59 | - run: sudo apt install -y socat 60 | - name: Run SSH Server 61 | run: docker run -d -p 2022:22 --init ssh-server 62 | - name: Run Nginx 63 | run: docker run -d -p 8888:80 nginx:alpine 64 | - name: Run Piping Server 65 | run: docker run -d -p 8080:8080 nwtgck/piping-server:v1.2.0 66 | - uses: actions/checkout@v4 67 | - name: Set up Go 1.x 68 | uses: actions/setup-go@v5 69 | with: 70 | go-version: '1.20' 71 | - run: CGO_ENABLED=0 go build -o piping-tunnel main/main.go 72 | 73 | - name: Normal tunnel 74 | run: | 75 | set -eux 76 | ./piping-tunnel -s http://localhost:8080 server -p 2022 aaa bbb & 77 | ./piping-tunnel -s http://localhost:8080 client -p 3322 aaa bbb & 78 | sleep 1 79 | # (base: -o option: https://www.cyberithub.com/ssh-host-key-verification-failed-error-in-linux/) 80 | ssh -p 3322 -o 'StrictHostKeyChecking no' guest@localhost hostname 81 | 82 | - name: Unix socket on server host side 83 | run: | 84 | set -eux 85 | socat UNIX-LISTEN:/tmp/my_nginx_socat TCP:localhost:8888 & 86 | sleep 1 87 | ./piping-tunnel -s http://localhost:8080 server --unix-socket=/tmp/my_nginx_socat aaa bbb & 88 | ./piping-tunnel -s http://localhost:8080 client -p 8889 aaa bbb & 89 | sleep 1 90 | curl localhost:8889 91 | 92 | - name: Unix socket on client host side 93 | run: | 94 | set -eux 95 | ./piping-tunnel -s http://localhost:8080 server -p 8888 aaa bbb & 96 | ./piping-tunnel -s http://localhost:8080 client --unix-socket=/tmp/my_unginx aaa bbb & 97 | sleep 1 98 | curl --unix-socket /tmp/my_unginx http:/index.html 99 | 100 | - name: Encrypt with AES-CTR 101 | run: | 102 | set -eux 103 | ./piping-tunnel -s http://localhost:8080 server -p 2022 --symmetric --cipher-type=aes-ctr --pass=mypass aesctraaa aesctrbbb & 104 | ./piping-tunnel -s http://localhost:8080 client -p 3322 --symmetric --cipher-type=aes-ctr --pass=mypass aesctraaa aesctrbbb & 105 | sleep 1 106 | # (base: -o option: https://www.cyberithub.com/ssh-host-key-verification-failed-error-in-linux/) 107 | ssh -p 3322 -o 'StrictHostKeyChecking no' guest@localhost hostname 108 | 109 | - name: Encrypt with OpenSSL-compabile AES-CTR 110 | run: | 111 | set -eux 112 | ./piping-tunnel -s http://localhost:8080 server -p 2022 --symmetric --cipher-type=openssl-aes-256-ctr --pbkdf2='{"iter":100000,"hash":"sha256"}' --pass=mypass openssl1aaa openssl1bbb & echo $! > pid1 113 | ./piping-tunnel -s http://localhost:8080 client -p 3322 --symmetric --cipher-type=openssl-aes-256-ctr --pbkdf2='{"iter":100000,"hash":"sha256"}' --pass=mypass openssl1aaa openssl1bbb & echo $! > pid2 114 | sleep 1 115 | # (base: -o option: https://www.cyberithub.com/ssh-host-key-verification-failed-error-in-linux/) 116 | ssh -p 3322 -o 'StrictHostKeyChecking no' guest@localhost hostname 117 | 118 | - name: Encrypt with OpenSSL-compabile AES-CTR using real openssl in server host 119 | run: | 120 | set -eux 121 | curl -sSN http://localhost:8080/openssl2aaa | stdbuf -i0 -o0 openssl aes-256-ctr -d -pass "pass:mypass" -bufsize 1 -pbkdf2 -iter 100000 -md sha256 | nc localhost 2022 | stdbuf -i0 -o0 openssl aes-256-ctr -pass "pass:mypass" -bufsize 1 -pbkdf2 -iter 100000 -md sha256 | curl -sSNT - http://localhost:8080/openssl2bbb & 122 | ./piping-tunnel -s http://localhost:8080 client -p 3322 --symmetric --cipher-type=openssl-aes-256-ctr --pbkdf2='{"iter":100000,"hash":"sha256"}' --pass=mypass openssl2aaa openssl2bbb & 123 | sleep 1 124 | # (base: -o option: https://www.cyberithub.com/ssh-host-key-verification-failed-error-in-linux/) 125 | ssh -p 3322 -o 'StrictHostKeyChecking no' guest@localhost hostname 126 | 127 | - name: Encrypt with OpenSSL-compabile AES-CTR using real openssl in client host 128 | run: | 129 | set -eux 130 | ./piping-tunnel -s http://localhost:8080 server -p 2022 --symmetric --cipher-type=openssl-aes-256-ctr --pbkdf2='{"iter":100000,"hash":"sha256"}' --pass=mypass openssl3aaa openssl3bbb & 131 | curl -NsS http://localhost:8080/openssl3bbb | stdbuf -i0 -o0 openssl aes-256-ctr -d -pass "pass:mypass" -bufsize 1 -pbkdf2 -iter 100000 -md sha256 | nc -l -p 3322 | stdbuf -i0 -o0 openssl aes-256-ctr -pass "pass:mypass" -bufsize 1 -pbkdf2 -iter 100000 -md sha256 | curl -NsST - http://localhost:8080/openssl3aaa & 132 | sleep 1 133 | # (base: -o option: https://www.cyberithub.com/ssh-host-key-verification-failed-error-in-linux/) 134 | ssh -p 3322 -o 'StrictHostKeyChecking no' guest@localhost hostname 135 | 136 | - name: yamux 137 | run: | 138 | set -eux 139 | # Run server-host with yamux 140 | ./piping-tunnel -s http://localhost:8080 server -p 2022 --yamux aaa-yamux bbb-yamux & echo $! > pid1 141 | # Run client-host with yamux 142 | ./piping-tunnel -s http://localhost:8080 client -p 4422 --yamux aaa-yamux bbb-yamux & echo $! > pid2 143 | sleep 1 144 | # Check whether ssh multiple times 145 | # (base: -o option: https://www.cyberithub.com/ssh-host-key-verification-failed-error-in-linux/) 146 | ssh -p 4422 -o 'StrictHostKeyChecking no' guest@localhost hostname 147 | ssh -p 4422 -o 'StrictHostKeyChecking no' guest@localhost ls -l / 148 | kill $(cat pid1) && kill $(cat pid2) 149 | 150 | - name: yamux (encrypt with AES-CTR) 151 | run: | 152 | set -eux 153 | # Run server-host with yamux (encrypt with AES-CTR) 154 | ./piping-tunnel -s http://localhost:8080 server -p 2022 --yamux --symmetric --cipher-type=aes-ctr --pass=mypass aaa-yamux bbb-yamux & echo $! > pid1 155 | # Run client-host with yamux (encrypt with AES-CTR) 156 | ./piping-tunnel -s http://localhost:8080 client -p 4422 --yamux --symmetric --cipher-type=aes-ctr --pass=mypass aaa-yamux bbb-yamux & echo $! > pid2 157 | sleep 1 158 | # Check whether ssh multiple times 159 | # (base: -o option: https://www.cyberithub.com/ssh-host-key-verification-failed-error-in-linux/) 160 | ssh -p 4422 -o 'StrictHostKeyChecking no' guest@localhost hostname 161 | ssh -p 4422 -o 'StrictHostKeyChecking no' guest@localhost ls -l / 162 | kill $(cat pid1) && kill $(cat pid2) 163 | 164 | - name: yamux SOCKS 165 | run: | 166 | set -eux 167 | # Run socks with yamux 168 | ./piping-tunnel -s http://localhost:8080 socks --yamux aaa-socks bbb-socks & echo $! > pid1 169 | # Run client-host with yamux 170 | ./piping-tunnel -s http://localhost:8080 client -p 1081 --yamux aaa-socks bbb-socks & echo $! > pid2 171 | sleep 1 172 | # NOTE: Depends on external resource: example.com 173 | curl -x socks5h://localhost:1081 https://example.com 174 | kill $(cat pid1) && kill $(cat pid2) 175 | 176 | - name: yamux SOCKS (encrypt with AES-CTR) 177 | run: | 178 | set -eux 179 | # Run socks with yamux (encrypt with AES-CTR) 180 | ./piping-tunnel -s http://localhost:8080 socks --yamux --symmetric --cipher-type=aes-ctr --pass=mypass aaa-socks bbb-socks & echo $! > pid1 181 | # Run client-host with yamux (encrypt with AES-CTR) 182 | ./piping-tunnel -s http://localhost:8080 client -p 1081 --yamux --symmetric --cipher-type=aes-ctr --pass=mypass aaa-socks bbb-socks & echo $! > pid2 183 | sleep 1 184 | # NOTE: Depends on external resource: example.com 185 | curl -x socks5h://localhost:1081 https://example.com 186 | kill $(cat pid1) && kill $(cat pid2) 187 | 188 | - name: pmux 189 | run: | 190 | set -eux 191 | # Run server-host1 with pmux 192 | ./piping-tunnel -s http://localhost:8080 server -p 2022 --pmux aaa-pmux bbb-pmux & echo $! > pid1 193 | sleep 1 194 | # pmux allows multiple clients in one set of paths 195 | # Run client-host1 with pmux 196 | ./piping-tunnel -s http://localhost:8080 client -p 5522 --pmux aaa-pmux bbb-pmux & echo $! > pid2 197 | # Run client-host2 with pmux 198 | ./piping-tunnel -s http://localhost:8080 client -p 6622 --pmux aaa-pmux bbb-pmux & echo $! > pid3 199 | sleep 2 200 | # Check whether ssh multiple times 201 | # (base: -o option: https://www.cyberithub.com/ssh-host-key-verification-failed-error-in-linux/) 202 | ssh -p 5522 -o 'StrictHostKeyChecking no' guest@localhost hostname 203 | ssh -p 5522 -o 'StrictHostKeyChecking no' guest@localhost ls -l / 204 | ssh -p 6622 -o 'StrictHostKeyChecking no' guest@localhost ls -l / 205 | kill $(cat pid1) && kill $(cat pid2) && kill $(cat pid3) 206 | -------------------------------------------------------------------------------- /cmd/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/hashicorp/yamux" 7 | "github.com/nwtgck/go-piping-tunnel/backoff" 8 | "github.com/nwtgck/go-piping-tunnel/cmd" 9 | "github.com/nwtgck/go-piping-tunnel/piping_util" 10 | "github.com/nwtgck/go-piping-tunnel/pmux" 11 | "github.com/nwtgck/go-piping-tunnel/util" 12 | "github.com/pkg/errors" 13 | "github.com/spf13/cobra" 14 | "io" 15 | "net" 16 | "net/http" 17 | "time" 18 | ) 19 | 20 | var flag struct { 21 | targetHost string 22 | serverHostPort int 23 | serverHostUnixSocket string 24 | clientToServerBufSize uint 25 | yamux bool 26 | pmux bool 27 | pmuxConfig string 28 | symmetricallyEncrypts bool 29 | symmetricallyEncryptPassphrase string 30 | cipherType string 31 | pbkdf2JsonString string 32 | } 33 | 34 | func init() { 35 | cmd.RootCmd.AddCommand(serverCmd) 36 | serverCmd.Flags().StringVarP(&flag.targetHost, "host", "", "localhost", "Target host") 37 | serverCmd.Flags().IntVarP(&flag.serverHostPort, "port", "p", 0, "TCP port of server host") 38 | serverCmd.Flags().StringVarP(&flag.serverHostUnixSocket, "unix-socket", "", "", "Unix socket of server host") 39 | serverCmd.Flags().UintVarP(&flag.clientToServerBufSize, "cs-buf-size", "", 4096, "Buffer size of client-to-server in bytes") 40 | serverCmd.Flags().BoolVarP(&flag.yamux, cmd.YamuxFlagLongName, "", false, "Multiplex connection by hashicorp/yamux") 41 | serverCmd.Flags().BoolVarP(&flag.pmux, cmd.PmuxFlagLongName, "", false, "Multiplex connection by pmux (experimental)") 42 | serverCmd.Flags().StringVarP(&flag.pmuxConfig, cmd.PmuxConfigFlagLongName, "", `{"hb": true}`, "pmux config in JSON (experimental)") 43 | serverCmd.Flags().BoolVarP(&flag.symmetricallyEncrypts, cmd.SymmetricallyEncryptsFlagLongName, cmd.SymmetricallyEncryptsFlagShortName, false, "Encrypt symmetrically") 44 | serverCmd.Flags().StringVarP(&flag.symmetricallyEncryptPassphrase, cmd.SymmetricallyEncryptPassphraseFlagLongName, "", "", "Passphrase for encryption") 45 | serverCmd.Flags().StringVarP(&flag.cipherType, cmd.CipherTypeFlagLongName, "", cmd.DefaultCipherType, fmt.Sprintf("Cipher type: %s, %s, %s, %s ", piping_util.CipherTypeAesCtr, piping_util.CipherTypeOpensslAes128Ctr, piping_util.CipherTypeOpensslAes256Ctr, piping_util.CipherTypeOpenpgp)) 46 | // NOTE: default value of --pbkdf2 should be empty to detect key derive derivation from multiple algorithms in the future. 47 | serverCmd.Flags().StringVarP(&flag.pbkdf2JsonString, cmd.Pbkdf2FlagLongName, "", "", fmt.Sprintf("e.g. %s", cmd.ExamplePbkdf2JsonStr())) 48 | } 49 | 50 | var serverCmd = &cobra.Command{ 51 | Use: "server", 52 | Short: "Run server-host", 53 | RunE: func(_ *cobra.Command, args []string) error { 54 | // Validate cipher-type 55 | if flag.symmetricallyEncrypts { 56 | if err := cmd.ValidateClientCipher(flag.cipherType); err != nil { 57 | return err 58 | } 59 | } 60 | clientToServerPath, serverToClientPath, err := cmd.GeneratePaths(args) 61 | if err != nil { 62 | return err 63 | } 64 | headers, err := piping_util.ParseKeyValueStrings(cmd.HeaderKeyValueStrs) 65 | if err != nil { 66 | return err 67 | } 68 | httpClient := util.CreateHttpClient(cmd.Insecure, cmd.HttpWriteBufSize, cmd.HttpReadBufSize) 69 | if cmd.DnsServer != "" { 70 | // Set DNS resolver 71 | httpClient.Transport.(*http.Transport).DialContext = util.CreateDialContext(cmd.DnsServer) 72 | } 73 | serverToClientUrl, err := util.UrlJoin(cmd.ServerUrl, serverToClientPath) 74 | if err != nil { 75 | return err 76 | } 77 | clientToServerUrl, err := util.UrlJoin(cmd.ServerUrl, clientToServerPath) 78 | if err != nil { 79 | return err 80 | } 81 | var opensslAesCtrParams *cmd.OpensslAesCtrParams = nil 82 | if flag.symmetricallyEncrypts { 83 | opensslAesCtrParams, err = cmd.ParseOpensslAesCtrParams(flag.cipherType, flag.pbkdf2JsonString) 84 | if err != nil { 85 | return err 86 | } 87 | } 88 | // Print hint 89 | printHintForClientHost(clientToServerUrl, serverToClientUrl, clientToServerPath, serverToClientPath, opensslAesCtrParams) 90 | // Make user input passphrase if it is empty 91 | if flag.symmetricallyEncrypts { 92 | err = cmd.MakeUserInputPassphraseIfEmpty(&flag.symmetricallyEncryptPassphrase) 93 | if err != nil { 94 | return err 95 | } 96 | } 97 | // Use multiplexer with yamux 98 | if flag.yamux { 99 | fmt.Println("[INFO] Multiplexing with hashicorp/yamux") 100 | return serverHandleWithYamux(httpClient, headers, clientToServerUrl, serverToClientUrl) 101 | } 102 | 103 | // If pmux is enabled 104 | if flag.pmux { 105 | fmt.Println("[INFO] Multiplexing with pmux") 106 | return serverHandleWithPmux(httpClient, headers, clientToServerUrl, serverToClientUrl) 107 | } 108 | 109 | conn, err := serverHostDial() 110 | if err != nil { 111 | return err 112 | } 113 | defer conn.Close() 114 | // If encryption is enabled 115 | if flag.symmetricallyEncrypts { 116 | var duplex io.ReadWriteCloser 117 | duplex, err := piping_util.DuplexConnect(httpClient, headers, serverToClientUrl, clientToServerUrl) 118 | if err != nil { 119 | return err 120 | } 121 | duplex, err = cmd.MakeDuplexWithEncryptionAndProgressIfNeed(duplex, flag.symmetricallyEncrypts, flag.symmetricallyEncryptPassphrase, flag.cipherType, flag.pbkdf2JsonString) 122 | if err != nil { 123 | return err 124 | } 125 | fin := make(chan error) 126 | go func() { 127 | // TODO: hard code 128 | var buf = make([]byte, 4096) 129 | _, err := io.CopyBuffer(duplex, conn, buf) 130 | fin <- err 131 | }() 132 | go func() { 133 | // TODO: hard code 134 | var buf = make([]byte, 4096) 135 | _, err := io.CopyBuffer(conn, duplex, buf) 136 | fin <- err 137 | }() 138 | return util.CombineErrors(<-fin, <-fin) 139 | } 140 | err = piping_util.HandleDuplex(httpClient, conn, headers, serverToClientUrl, clientToServerUrl, flag.clientToServerBufSize, nil, cmd.ShowProgress, cmd.MakeProgressMessage) 141 | fmt.Println() 142 | if err != nil { 143 | return err 144 | } 145 | fmt.Println("[INFO] Finished") 146 | 147 | return nil 148 | }, 149 | } 150 | 151 | func serverHostDial() (net.Conn, error) { 152 | if flag.serverHostUnixSocket == "" { 153 | return net.Dial("tcp", fmt.Sprintf("%s:%d", flag.targetHost, flag.serverHostPort)) 154 | } else { 155 | return net.Dial("unix", flag.serverHostUnixSocket) 156 | } 157 | } 158 | 159 | func printHintForClientHost(clientToServerUrl string, serverToClientUrl string, clientToServerPath string, serverToClientPath string, opensslAesCtrParams *cmd.OpensslAesCtrParams) { 160 | if !flag.yamux && !flag.pmux { 161 | if flag.symmetricallyEncrypts { 162 | if opensslAesCtrParams != nil { 163 | fmt.Println("[INFO] Hint: Client host. Port 31376 may be replaced (socat + curl + openssl)") 164 | fmt.Printf( 165 | " read -p \"passphrase: \" -s pass && curl -NsS %s | stdbuf -i0 -o0 openssl aes-%d-ctr -d -pass \"pass:$pass\" -bufsize 1 -pbkdf2 -iter %d -md %s | socat TCP-LISTEN:31376 - | stdbuf -i0 -o0 openssl aes-%d-ctr -pass \"pass:$pass\" -bufsize 1 -pbkdf2 -iter %d -md %s | curl -NsST - %s; unset pass\n", 166 | serverToClientUrl, 167 | opensslAesCtrParams.KeyBits, 168 | opensslAesCtrParams.Pbkdf2.Iter, 169 | opensslAesCtrParams.Pbkdf2.HashNameForCommandHint, 170 | opensslAesCtrParams.KeyBits, 171 | opensslAesCtrParams.Pbkdf2.Iter, 172 | opensslAesCtrParams.Pbkdf2.HashNameForCommandHint, 173 | clientToServerUrl, 174 | ) 175 | } 176 | } else { 177 | fmt.Println("[INFO] Hint: Client host (socat + curl)") 178 | // NOTE: nc can be used instead of socat but nc has variant: `nc -l 31376` in BSD version, `nc -l -p 31376` in GNU version. 179 | fmt.Printf(" curl -NsS %s | socat TCP-LISTEN:31376 - | curl -NsST - %s\n", serverToClientUrl, clientToServerUrl) 180 | } 181 | } 182 | flags := "" 183 | if flag.symmetricallyEncrypts { 184 | flags += fmt.Sprintf("-%s ", cmd.SymmetricallyEncryptsFlagShortName) 185 | flags += fmt.Sprintf("--%s=%s ", cmd.CipherTypeFlagLongName, flag.cipherType) 186 | switch flag.cipherType { 187 | case piping_util.CipherTypeOpensslAes128Ctr: 188 | fallthrough 189 | case piping_util.CipherTypeOpensslAes256Ctr: 190 | flags += fmt.Sprintf("--%s='%s' ", cmd.Pbkdf2FlagLongName, flag.pbkdf2JsonString) 191 | } 192 | } 193 | if flag.yamux { 194 | flags += fmt.Sprintf("--%s ", cmd.YamuxFlagLongName) 195 | } 196 | if flag.pmux { 197 | flags += fmt.Sprintf("--%s ", cmd.PmuxFlagLongName) 198 | } 199 | fmt.Println("[INFO] Hint: Client host (piping-tunnel)") 200 | fmt.Printf( 201 | " piping-tunnel -s %s client -p 31376 %s%s %s\n", 202 | cmd.ServerUrl, 203 | flags, 204 | clientToServerPath, 205 | serverToClientPath, 206 | ) 207 | } 208 | 209 | func serverHandleWithYamux(httpClient *http.Client, headers []piping_util.KeyValue, clientToServerUrl string, serverToClientUrl string) error { 210 | var duplex io.ReadWriteCloser 211 | duplex, err := piping_util.DuplexConnectWithHandlers( 212 | func(body io.Reader) (*http.Response, error) { 213 | return piping_util.PipingSend(httpClient, cmd.HeadersWithYamux(headers), serverToClientUrl, body) 214 | }, 215 | func() (*http.Response, error) { 216 | res, err := piping_util.PipingGet(httpClient, headers, clientToServerUrl) 217 | if err != nil { 218 | return nil, err 219 | } 220 | contentType := res.Header.Get("Content-Type") 221 | if contentType != cmd.YamuxMimeType { 222 | fmt.Printf("[WARN] --%s flag may be missing in client-host\n", cmd.YamuxFlagLongName) 223 | } 224 | return res, nil 225 | }, 226 | ) 227 | if err != nil { 228 | return err 229 | } 230 | duplex, err = cmd.MakeDuplexWithEncryptionAndProgressIfNeed(duplex, flag.symmetricallyEncrypts, flag.symmetricallyEncryptPassphrase, flag.cipherType, flag.pbkdf2JsonString) 231 | if err != nil { 232 | return err 233 | } 234 | yamuxSession, err := yamux.Server(duplex, nil) 235 | if err != nil { 236 | return err 237 | } 238 | for { 239 | yamuxStream, err := yamuxSession.Accept() 240 | if err != nil { 241 | return err 242 | } 243 | conn, err := serverHostDial() 244 | if err != nil { 245 | return err 246 | } 247 | fin := make(chan struct{}) 248 | go func() { 249 | // TODO: hard code 250 | var buf = make([]byte, 4096) 251 | io.CopyBuffer(yamuxStream, conn, buf) 252 | fin <- struct{}{} 253 | }() 254 | go func() { 255 | // TODO: hard code 256 | var buf = make([]byte, 4096) 257 | io.CopyBuffer(conn, yamuxStream, buf) 258 | fin <- struct{}{} 259 | }() 260 | go func() { 261 | <-fin 262 | <-fin 263 | close(fin) 264 | conn.Close() 265 | yamuxStream.Close() 266 | }() 267 | } 268 | } 269 | 270 | func dialLoop() net.Conn { 271 | b := backoff.NewExponentialBackoff() 272 | for { 273 | conn, err := serverHostDial() 274 | if err != nil { 275 | // backoff 276 | time.Sleep(b.NextDuration()) 277 | continue 278 | } 279 | return conn 280 | } 281 | } 282 | 283 | func serverHandleWithPmux(httpClient *http.Client, headers []piping_util.KeyValue, clientToServerUrl string, serverToClientUrl string) error { 284 | var config cmd.ServerPmuxConfigJson 285 | if json.Unmarshal([]byte(flag.pmuxConfig), &config) != nil { 286 | return errors.Errorf("invalid pmux config format") 287 | } 288 | pmuxServer := pmux.Server(httpClient, headers, serverToClientUrl, clientToServerUrl, config.Hb, flag.symmetricallyEncrypts, flag.symmetricallyEncryptPassphrase, flag.cipherType) 289 | for { 290 | stream, err := pmuxServer.Accept() 291 | if err != nil { 292 | cmd.Vlog.Log( 293 | fmt.Sprintf("error(pmux accept): %v", errors.WithStack(err)), 294 | fmt.Sprintf("error(pmux accept): %+v", errors.WithStack(err)), 295 | ) 296 | continue 297 | } 298 | conn := dialLoop() 299 | go func() { 300 | // TODO: hard code 301 | var buf = make([]byte, 4096) 302 | _, err := io.CopyBuffer(conn, stream, buf) 303 | if err != nil { 304 | cmd.Vlog.Log( 305 | fmt.Sprintf("error(pmux stream → conn): %v", errors.WithStack(err)), 306 | fmt.Sprintf("error(pmux stream → conn): %+v", errors.WithStack(err)), 307 | ) 308 | conn.Close() 309 | return 310 | } 311 | }() 312 | 313 | go func() { 314 | // TODO: hard code 315 | var buf = make([]byte, 4096) 316 | _, err := io.CopyBuffer(stream, conn, buf) 317 | if err != nil { 318 | cmd.Vlog.Log( 319 | fmt.Sprintf("error(conn → pmux stream): %v", errors.WithStack(err)), 320 | fmt.Sprintf("error(conn → pmux stream): %+v", errors.WithStack(err)), 321 | ) 322 | conn.Close() 323 | return 324 | } 325 | }() 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /cmd/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/hashicorp/yamux" 7 | "github.com/nwtgck/go-piping-tunnel/cmd" 8 | "github.com/nwtgck/go-piping-tunnel/piping_util" 9 | "github.com/nwtgck/go-piping-tunnel/pmux" 10 | "github.com/nwtgck/go-piping-tunnel/util" 11 | "github.com/nwtgck/go-piping-tunnel/version" 12 | "github.com/pkg/errors" 13 | "github.com/spf13/cobra" 14 | "io" 15 | "net" 16 | "net/http" 17 | "strconv" 18 | ) 19 | 20 | var flag struct { 21 | clientHostPort int 22 | clientHostUnixSocket string 23 | serverToClientBufSize uint 24 | yamux bool 25 | pmux bool 26 | pmuxConfig string 27 | symmetricallyEncrypts bool 28 | symmetricallyEncryptPassphrase string 29 | cipherType string 30 | pbkdf2JsonString string 31 | } 32 | 33 | func init() { 34 | cmd.RootCmd.AddCommand(clientCmd) 35 | clientCmd.Flags().IntVarP(&flag.clientHostPort, "port", "p", 0, "TCP port of client host") 36 | clientCmd.Flags().StringVarP(&flag.clientHostUnixSocket, "unix-socket", "", "", "Unix socket of client host") 37 | clientCmd.Flags().UintVarP(&flag.serverToClientBufSize, "sc-buf-size", "", 4096, "Buffer size of server-to-client in bytes") 38 | clientCmd.Flags().BoolVarP(&flag.yamux, cmd.YamuxFlagLongName, "", false, "Multiplex connection by hashicorp/yamux") 39 | clientCmd.Flags().BoolVarP(&flag.pmux, cmd.PmuxFlagLongName, "", false, "Multiplex connection by pmux (experimental)") 40 | clientCmd.Flags().StringVarP(&flag.pmuxConfig, cmd.PmuxConfigFlagLongName, "", `{"hb": true}`, "pmux config in JSON (experimental)") 41 | clientCmd.Flags().BoolVarP(&flag.symmetricallyEncrypts, cmd.SymmetricallyEncryptsFlagLongName, cmd.SymmetricallyEncryptsFlagShortName, false, "Encrypt symmetrically") 42 | clientCmd.Flags().StringVarP(&flag.symmetricallyEncryptPassphrase, cmd.SymmetricallyEncryptPassphraseFlagLongName, "", "", "Passphrase for encryption") 43 | clientCmd.Flags().StringVarP(&flag.cipherType, cmd.CipherTypeFlagLongName, "", cmd.DefaultCipherType, fmt.Sprintf("Cipher type: %s, %s, %s, %s ", piping_util.CipherTypeAesCtr, piping_util.CipherTypeOpensslAes128Ctr, piping_util.CipherTypeOpensslAes256Ctr, piping_util.CipherTypeOpenpgp)) 44 | // NOTE: default value of --pbkdf2 should be empty to detect key derive derivation from multiple algorithms in the future. 45 | clientCmd.Flags().StringVarP(&flag.pbkdf2JsonString, cmd.Pbkdf2FlagLongName, "", "", fmt.Sprintf("e.g. %s", cmd.ExamplePbkdf2JsonStr())) 46 | } 47 | 48 | var clientCmd = &cobra.Command{ 49 | Use: "client", 50 | Short: "Run client-host", 51 | RunE: func(_ *cobra.Command, args []string) error { 52 | // Validate cipher-type 53 | if flag.symmetricallyEncrypts { 54 | if err := cmd.ValidateClientCipher(flag.cipherType); err != nil { 55 | return err 56 | } 57 | } 58 | clientToServerPath, serverToClientPath, err := cmd.GeneratePaths(args) 59 | if err != nil { 60 | return err 61 | } 62 | headers, err := piping_util.ParseKeyValueStrings(cmd.HeaderKeyValueStrs) 63 | if err != nil { 64 | return err 65 | } 66 | httpClient := util.CreateHttpClient(cmd.Insecure, cmd.HttpWriteBufSize, cmd.HttpReadBufSize) 67 | if cmd.DnsServer != "" { 68 | // Set DNS resolver 69 | httpClient.Transport.(*http.Transport).DialContext = util.CreateDialContext(cmd.DnsServer) 70 | } 71 | clientToServerUrl, err := util.UrlJoin(cmd.ServerUrl, clientToServerPath) 72 | if err != nil { 73 | return err 74 | } 75 | serverToClientUrl, err := util.UrlJoin(cmd.ServerUrl, serverToClientPath) 76 | if err != nil { 77 | return err 78 | } 79 | var ln net.Listener 80 | if flag.clientHostUnixSocket == "" { 81 | ln, err = net.Listen("tcp", fmt.Sprintf(":%d", flag.clientHostPort)) 82 | } else { 83 | ln, err = net.Listen("unix", flag.clientHostUnixSocket) 84 | } 85 | if err != nil { 86 | return err 87 | } 88 | var opensslAesCtrParams *cmd.OpensslAesCtrParams = nil 89 | if flag.symmetricallyEncrypts { 90 | opensslAesCtrParams, err = cmd.ParseOpensslAesCtrParams(flag.cipherType, flag.pbkdf2JsonString) 91 | if err != nil { 92 | return err 93 | } 94 | } 95 | // Print hint 96 | printHintForServerHost(ln, clientToServerUrl, serverToClientUrl, clientToServerPath, serverToClientPath, opensslAesCtrParams) 97 | // Make user input passphrase if it is empty 98 | if flag.symmetricallyEncrypts { 99 | err = cmd.MakeUserInputPassphraseIfEmpty(&flag.symmetricallyEncryptPassphrase) 100 | if err != nil { 101 | return err 102 | } 103 | } 104 | // Use multiplexer with yamux 105 | if flag.yamux { 106 | fmt.Println("[INFO] Multiplexing with hashicorp/yamux") 107 | return clientHandleWithYamux(ln, httpClient, headers, clientToServerUrl, serverToClientUrl) 108 | } 109 | // If pmux is enabled 110 | if flag.pmux { 111 | fmt.Println("[INFO] Multiplexing with pmux") 112 | return clientHandleWithPmux(ln, httpClient, headers, clientToServerUrl, serverToClientUrl) 113 | } 114 | conn, err := ln.Accept() 115 | if err != nil { 116 | return err 117 | } 118 | fmt.Println("[INFO] accepted") 119 | // Refuse another new connection 120 | ln.Close() 121 | // If encryption is enabled 122 | if flag.symmetricallyEncrypts { 123 | var duplex io.ReadWriteCloser 124 | duplex, err := piping_util.DuplexConnect(httpClient, headers, clientToServerUrl, serverToClientUrl) 125 | if err != nil { 126 | return err 127 | } 128 | duplex, err = cmd.MakeDuplexWithEncryptionAndProgressIfNeed(duplex, flag.symmetricallyEncrypts, flag.symmetricallyEncryptPassphrase, flag.cipherType, flag.pbkdf2JsonString) 129 | if err != nil { 130 | return err 131 | } 132 | fin := make(chan error) 133 | go func() { 134 | // TODO: hard code 135 | var buf = make([]byte, 4096) 136 | _, err := io.CopyBuffer(duplex, conn, buf) 137 | fin <- err 138 | }() 139 | go func() { 140 | // TODO: hard code 141 | var buf = make([]byte, 4096) 142 | _, err := io.CopyBuffer(conn, duplex, buf) 143 | fin <- err 144 | }() 145 | return util.CombineErrors(<-fin, <-fin) 146 | } 147 | err = piping_util.HandleDuplex(httpClient, conn, headers, clientToServerUrl, serverToClientUrl, flag.serverToClientBufSize, nil, cmd.ShowProgress, cmd.MakeProgressMessage) 148 | fmt.Println() 149 | if err != nil { 150 | return err 151 | } 152 | fmt.Println("[INFO] Finished") 153 | 154 | return nil 155 | }, 156 | } 157 | 158 | func printHintForServerHost(ln net.Listener, clientToServerUrl string, serverToClientUrl string, clientToServerPath string, serverToClientPath string, opensslAesCtrParams *cmd.OpensslAesCtrParams) { 159 | var listeningOn string 160 | if addr, ok := ln.Addr().(*net.TCPAddr); ok { 161 | // (base: https://stackoverflow.com/a/43425461) 162 | flag.clientHostPort = addr.Port 163 | listeningOn = strconv.Itoa(addr.Port) 164 | } else { 165 | listeningOn = flag.clientHostUnixSocket 166 | } 167 | fmt.Printf("[INFO] Client host listening on %s ...\n", listeningOn) 168 | if !flag.yamux && !flag.pmux { 169 | if flag.symmetricallyEncrypts { 170 | if opensslAesCtrParams != nil { 171 | fmt.Println("[INFO] Hint: Server host. should be replaced (nc + curl + openssl)") 172 | fmt.Printf( 173 | " read -p \"passphrase: \" -s pass && curl -sSN %s | stdbuf -i0 -o0 openssl aes-%d-ctr -d -pass \"pass:$pass\" -bufsize 1 -pbkdf2 -iter %d -md %s | nc 127.0.0.1 | stdbuf -i0 -o0 openssl aes-%d-ctr -pass \"pass:$pass\" -bufsize 1 -pbkdf2 -iter %d -md %s | curl -sSNT - %s; unset pass\n", 174 | clientToServerUrl, 175 | opensslAesCtrParams.KeyBits, 176 | opensslAesCtrParams.Pbkdf2.Iter, 177 | opensslAesCtrParams.Pbkdf2.HashNameForCommandHint, 178 | opensslAesCtrParams.KeyBits, 179 | opensslAesCtrParams.Pbkdf2.Iter, 180 | opensslAesCtrParams.Pbkdf2.HashNameForCommandHint, 181 | serverToClientUrl, 182 | ) 183 | } 184 | } else { 185 | fmt.Println("[INFO] Hint: Server host (nc + curl)") 186 | fmt.Printf(" curl -sSN %s | nc 127.0.0.1 | curl -sSNT - %s\n", clientToServerUrl, serverToClientUrl) 187 | } 188 | } 189 | fmt.Println("[INFO] Hint: Server host (piping-tunnel)") 190 | flags := "" 191 | if flag.symmetricallyEncrypts { 192 | flags += fmt.Sprintf("-%s ", cmd.SymmetricallyEncryptsFlagShortName) 193 | flags += fmt.Sprintf("--%s=%s ", cmd.CipherTypeFlagLongName, flag.cipherType) 194 | switch flag.cipherType { 195 | case piping_util.CipherTypeOpensslAes128Ctr: 196 | fallthrough 197 | case piping_util.CipherTypeOpensslAes256Ctr: 198 | flags += fmt.Sprintf("--%s='%s' ", cmd.Pbkdf2FlagLongName, flag.pbkdf2JsonString) 199 | } 200 | } 201 | if flag.yamux { 202 | flags += fmt.Sprintf("--%s ", cmd.YamuxFlagLongName) 203 | } 204 | if flag.pmux { 205 | flags += fmt.Sprintf("--%s ", cmd.PmuxFlagLongName) 206 | } 207 | fmt.Printf( 208 | " piping-tunnel -s %s server -p %s%s %s\n", 209 | cmd.ServerUrl, 210 | flags, 211 | clientToServerPath, 212 | serverToClientPath, 213 | ) 214 | fmt.Println(" OR") 215 | fmt.Printf( 216 | " piping-tunnel -s %s socks %s%s %s\n", 217 | cmd.ServerUrl, 218 | flags, 219 | clientToServerPath, 220 | serverToClientPath, 221 | ) 222 | } 223 | 224 | func clientHandleWithYamux(ln net.Listener, httpClient *http.Client, headers []piping_util.KeyValue, clientToServerUrl string, serverToClientUrl string) error { 225 | var duplex io.ReadWriteCloser 226 | duplex, err := piping_util.DuplexConnectWithHandlers( 227 | func(body io.Reader) (*http.Response, error) { 228 | return piping_util.PipingSend(httpClient, cmd.HeadersWithYamux(headers), clientToServerUrl, body) 229 | }, 230 | func() (*http.Response, error) { 231 | res, err := piping_util.PipingGet(httpClient, headers, serverToClientUrl) 232 | if err != nil { 233 | return nil, err 234 | } 235 | contentType := res.Header.Get("Content-Type") 236 | if contentType != cmd.YamuxMimeType { 237 | fmt.Printf("[INFO] --%s flag may be missing in server-host\n", cmd.YamuxFlagLongName) 238 | } 239 | return res, nil 240 | }, 241 | ) 242 | if err != nil { 243 | return err 244 | } 245 | duplex, err = cmd.MakeDuplexWithEncryptionAndProgressIfNeed(duplex, flag.symmetricallyEncrypts, flag.symmetricallyEncryptPassphrase, flag.cipherType, flag.pbkdf2JsonString) 246 | if err != nil { 247 | return err 248 | } 249 | yamuxSession, err := yamux.Client(duplex, nil) 250 | if err != nil { 251 | return err 252 | } 253 | 254 | for { 255 | conn, err := ln.Accept() 256 | if err != nil { 257 | return err 258 | } 259 | yamuxStream, err := yamuxSession.Open() 260 | if err != nil { 261 | return err 262 | } 263 | fin := make(chan struct{}) 264 | go func() { 265 | // TODO: hard code 266 | var buf = make([]byte, 4096) 267 | io.CopyBuffer(yamuxStream, conn, buf) 268 | fin <- struct{}{} 269 | }() 270 | go func() { 271 | // TODO: hard code 272 | var buf = make([]byte, 4096) 273 | io.CopyBuffer(conn, yamuxStream, buf) 274 | fin <- struct{}{} 275 | }() 276 | go func() { 277 | <-fin 278 | <-fin 279 | close(fin) 280 | conn.Close() 281 | yamuxStream.Close() 282 | }() 283 | } 284 | } 285 | 286 | func clientHandleWithPmux(ln net.Listener, httpClient *http.Client, headers []piping_util.KeyValue, clientToServerUrl string, serverToClientUrl string) error { 287 | var config cmd.ClientPmuxConfigJson 288 | if json.Unmarshal([]byte(flag.pmuxConfig), &config) != nil { 289 | return errors.Errorf("invalid pmux config format") 290 | } 291 | pmuxClient, err := pmux.Client(httpClient, headers, clientToServerUrl, serverToClientUrl, config.Hb, flag.symmetricallyEncrypts, flag.symmetricallyEncryptPassphrase, flag.cipherType) 292 | if err != nil { 293 | if err == pmux.NonPmuxMimeTypeError { 294 | return errors.Errorf("--%s may be missing in server", cmd.PmuxFlagLongName) 295 | } 296 | if err == pmux.IncompatiblePmuxVersion { 297 | return errors.Errorf("%s, hint: use the same piping-tunnel version (current: %s)", err.Error(), version.Version) 298 | } 299 | if err == pmux.IncompatibleServerConfigError { 300 | return errors.Errorf("%s, hint: use the same piping-tunnel version (current: %s)", err.Error(), version.Version) 301 | } 302 | return err 303 | } 304 | for { 305 | conn, err := ln.Accept() 306 | if err != nil { 307 | cmd.Vlog.Log( 308 | fmt.Sprintf("error(accept): %v", errors.WithStack(err)), 309 | fmt.Sprintf("error(accept): %+v", errors.WithStack(err)), 310 | ) 311 | continue 312 | } 313 | stream, err := pmuxClient.Open() 314 | if err != nil { 315 | cmd.Vlog.Log( 316 | fmt.Sprintf("error(pmux open): %v", errors.WithStack(err)), 317 | fmt.Sprintf("error(pmux open): %+v", errors.WithStack(err)), 318 | ) 319 | continue 320 | } 321 | fin := make(chan struct{}) 322 | go func() { 323 | // TODO: hard code 324 | var buf = make([]byte, 4096) 325 | _, err := io.CopyBuffer(conn, stream, buf) 326 | fin <- struct{}{} 327 | if err != nil { 328 | cmd.Vlog.Log( 329 | fmt.Sprintf("error(pmux stream → conn): %v", errors.WithStack(err)), 330 | fmt.Sprintf("error(pmux stream → conn): %+v", errors.WithStack(err)), 331 | ) 332 | return 333 | } 334 | }() 335 | 336 | go func() { 337 | // TODO: hard code 338 | var buf = make([]byte, 4096) 339 | _, err := io.CopyBuffer(stream, conn, buf) 340 | fin <- struct{}{} 341 | if err != nil { 342 | cmd.Vlog.Log( 343 | fmt.Sprintf("error(conn → pmux stream): %v", errors.WithStack(err)), 344 | fmt.Sprintf("error(conn → pmux stream): %+v", errors.WithStack(err)), 345 | ) 346 | return 347 | } 348 | }() 349 | 350 | go func() { 351 | <-fin 352 | <-fin 353 | conn.Close() 354 | stream.Close() 355 | close(fin) 356 | }() 357 | } 358 | return nil 359 | } 360 | --------------------------------------------------------------------------------