diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..59980d1 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,23 @@ +--- +name: build # this string appears on badge +on: + - push + - pull_request +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + name: "Unit tests on go ${{ matrix.go }}" + strategy: + matrix: + go: + - "1.21" + - "1.22" + - "1.23" + - "1.24" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "${{ matrix.go }}" + - run: "go build -v -x ." diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..4eb28e9 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,15 @@ +--- +name: lint # this string appears on badge +on: + - push + - pull_request +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + name: "Linting" + steps: + - uses: actions/checkout@v4 + - uses: golangci/golangci-lint-action@v6 + with: + version: "v1.64" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..1655fff --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,21 @@ +--- +name: test # this string appears on badge +on: + - push + - pull_request +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + name: "Unit tests on go ${{ matrix.go }}" + strategy: + matrix: + go: + - "1.23" + - "1.24" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "${{ matrix.go }}" + - run: "go test -v -cover -timeout=5s ." diff --git a/.golangci.yml b/.golangci.yml index cf01fa4..8603c5a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,34 +1,26 @@ -run: - deadline: 1m - tests: false - #skip-files: - # - ".*\\.gen\\.go" - linters-settings: - golint: - min-confidence: 0 - maligned: - suggest-new: true goconst: min-len: 5 min-occurrences: 4 misspell: locale: US + gosec: + excludes: [G402] + ireturn: + allow: + - context.Context + - error + - io.(Reader|Writer) linters: - disable-all: true - enable: - - goconst - - misspell - - deadcode - - misspell - - structcheck - - errcheck - - unused - - varcheck - - staticcheck - - unconvert - - gofmt - - goimports - - golint - - ineffassign + enable-all: true + disable: + - depguard + - exhaustruct + - godox # TODOs is ok + - mnd # nice to have + - nlreturn # nice to have + - paralleltest # for now we have only one integration (not parallelable) test + - tenv + - varnamelen + - wsl diff --git a/README.md b/README.md index 4c6cf4f..c21ecb5 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ SSH Connection proxified with QUIC ┌───────────────────────────────────────┐ ┌───────────────────────┐ │ bob │ │ wopr │ │ ┌───────────────────────────────────┐ │ │ ┌───────────────────┐ │ -│ │ssh -o ProxyCommand "quicssh client│ │ │ │ sshd │ │ +│ │ssh -o ProxyCommand="quicssh client│ │ │ │ sshd │ │ │ │ --addr %h:4545" user@wopr │ │ │ └───────────────────┘ │ │ │ │ │ │ ▲ │ │ └───────────────────────────────────┘ │ │ │ │ @@ -50,37 +50,38 @@ SSH Connection proxified with QUIC ```console $ quicssh -h NAME: - quicssh - A new cli application + quicssh - Client and server parts to proxy SSH (TCP) over UDP using QUIC transport USAGE: - quicssh [global options] command [command options] [arguments...] + quicssh [global options] command [command options] VERSION: - 0.0.0 + v0.0.0-20230730133128-1c771b69d1a7+dirty COMMANDS: - server - client - help, h Shows a list of commands or help for one command + server + client + help, h Shows a list of commands or help for one command GLOBAL OPTIONS: - --help, -h show help (default: false) - --version, -v print the version (default: false) - ``` + --help, -h show help + --version, -v print the version +``` ### Client ```console $ quicssh client -h NAME: - quicssh client - + quicssh client USAGE: - quicssh client [command options] [arguments...] + quicssh client [command options] OPTIONS: - --addr value (default: "localhost:4242") - --help, -h show help (default: false) + --addr value address of server (default: "localhost:4242") + --localaddr value source address of UDP packets (default: ":0") + --help, -h show help ``` ### Server @@ -88,14 +89,15 @@ OPTIONS: ```console $ quicssh server -h NAME: - quicssh server - + quicssh server USAGE: - quicssh server [command options] [arguments...] + quicssh server [command options] OPTIONS: - --bind value (default: "localhost:4242") - --help, -h show help (default: false) + --bind value bind address (default: "localhost:4242") + --sshdaddr value target address of sshd (default: "localhost:22") + --help, -h show help ``` ## Install diff --git a/client.go b/client.go index eaedf20..beee3e5 100644 --- a/client.go +++ b/client.go @@ -2,56 +2,62 @@ package main import ( "crypto/tls" - "log" - "os" - "sync" + "net" + "time" quic "github.com/quic-go/quic-go" cli "github.com/urfave/cli/v2" - "golang.org/x/net/context" ) func client(c *cli.Context) error { - ctx, cancel := context.WithCancel(context.Background()) + ctx := withLabel(c.Context, "client") config := &tls.Config{ InsecureSkipVerify: true, NextProtos: []string{"quicssh"}, } - log.Printf("Dialing %q...", c.String("addr")) - session, err := quic.DialAddr(ctx, c.String("addr"), config, nil) + udpAddr, err := net.ResolveUDPAddr("udp", c.String("addr")) if err != nil { - return err + return er(ctx, err) + } + srcAddr, err := net.ResolveUDPAddr("udp", c.String("localaddr")) + if err != nil { + return er(ctx, err) + } + + logf(ctx, "Dialing %q->%q...", srcAddr.String(), udpAddr.String()) + conn, err := net.ListenUDP("udp", srcAddr) + if err != nil { + return er(ctx, err) + } + quicConfig := &quic.Config{MaxIdleTimeout: 10 * time.Second, KeepAlivePeriod: 5 * time.Second} + session, err := quic.Dial(ctx, conn, udpAddr, config, quicConfig) + if err != nil { + return er(ctx, err) } defer func() { if err := session.CloseWithError(0, "close"); err != nil { - log.Printf("session close error: %v", err) + logf(ctx, "session close error: %v", err) } }() - log.Printf("Opening stream sync...") + logf(ctx, "Opening stream sync...") stream, err := session.OpenStreamSync(ctx) if err != nil { - return err + return er(ctx, err) } + defer stream.Close() - log.Printf("Piping stream with QUIC...") - var wg sync.WaitGroup - wg.Add(3) - c1 := readAndWrite(ctx, stream, os.Stdout, &wg) - c2 := readAndWrite(ctx, os.Stdin, stream, &wg) + logf(ctx, "Piping stream with QUIC...") + c1 := readAndWrite(withLabel(ctx, "stdout"), stream, c.App.Writer) // App.Writer is stdout + c2 := readAndWrite(withLabel(ctx, "stdin"), c.App.Reader, stream) // App.Reader is stdin select { case err = <-c1: - if err != nil { - return err - } case err = <-c2: - if err != nil { - return err - } } - cancel() - wg.Wait() + if err != nil { + return err + } return nil } diff --git a/go.mod b/go.mod index bf7b006..881fa6c 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,27 @@ module moul.io/quicssh -go 1.20 +go 1.23.0 + +toolchain go1.24.0 require ( - github.com/quic-go/quic-go v0.35.1 - github.com/urfave/cli/v2 v2.25.6 - golang.org/x/net v0.11.0 + github.com/quic-go/quic-go v0.50.0 + github.com/urfave/cli/v2 v2.27.6 + golang.org/x/net v0.37.0 ) require ( - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/golang/mock v1.6.0 // indirect github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect github.com/onsi/ginkgo/v2 v2.10.0 // indirect - github.com/quic-go/qtls-go1-19 v0.3.2 // indirect - github.com/quic-go/qtls-go1-20 v0.2.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/crypto v0.10.0 // indirect - golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect - golang.org/x/mod v0.11.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/tools v0.10.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/tools v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index f56731a..5c23b8c 100644 --- a/go.sum +++ b/go.sum @@ -1,74 +1,59 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs= github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE= github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= +github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= -github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= -github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E= -github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= -github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo= -github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g= +github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= +github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/urfave/cli/v2 v2.25.6 h1:yuSkgDSZfH3L1CjF2/5fNNg2KbM47pY2EvjBq4ESQnU= -github.com/urfave/cli/v2 v2.25.6/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= -golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..750ead3 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,112 @@ +package main + +import ( + "bufio" + "errors" + "flag" + "io" + "net" + "strings" + "testing" + + cli "github.com/urfave/cli/v2" +) + +const ( + sshdAddr = "localhost:8822" + bindAddr = "localhost:4242" +) + +func TestInegratinGoldenFlow(t *testing.T) { + // run tcp-echo server (sort of sshd) + closer := tcpEchoServer() + defer closer() // close fake sshd + + // run quicssh server + serverContext := flags("sshdaddr", sshdAddr, "bind", bindAddr) + go func() { + err := server(serverContext) // TODO: make it able to shutdown gracefully, check error + if strings.Contains(err.Error(), "address already in use") { + panic(err.Error()) // cheapest way to fail test from goroutine + } + }() + + // run quicssh client + clientContext := flags("addr", bindAddr, "localaddr", ":0") + wr, rd := tweaksStdIO(clientContext) + go func() { + err := client(clientContext) // TODO: shutdownable, check error + if !errors.Is(err, io.EOF) { + panic(err.Error()) + } + }() + + // writing data to client + _, err := wr.Write([]byte("SSH SESSION\n")) // writing one line + noerr(err) + + // reading echo-data from client<-proxyserver<-echotcpserver<-proxyserver + lineReader := bufio.NewReader(rd) + line, err := lineReader.ReadString('\n') // waiting for just one echo-line back + noerr(err) + + // doing checks + const expected = "echo: SSH SESSION\n" + if line != expected { + t.Fatalf("expected %q, got %q", expected, line) + } +} + +func tweaksStdIO(c *cli.Context) (io.Writer, io.Reader) { + r1, w1 := io.Pipe() + r2, w2 := io.Pipe() + c.App.Reader = r1 + c.App.Writer = w2 + return w1, r2 // returning the opposite end of pipes +} + +func flags(pairs ...string) *cli.Context { + fs := flag.NewFlagSet("", 0) + for i := 0; i < len(pairs); i += 2 { + fs.String(pairs[i], pairs[i+1], "") + } + return cli.NewContext(cli.NewApp(), fs, nil) +} + +func tcpEchoServer() func() { + listener, err := net.Listen("tcp", sshdAddr) + noerr(err) + + go func() { + for { + conn, err := listener.Accept() + // Because historically they have not exported the error that they return. See issues #4373 and #19252. + if e, ok := err.(*net.OpError); ok && e.Unwrap().Error() == "use of closed network connection" { //nolint:errorlint + break + } + noerr(err) + // naive approach without any goroutines + reader := bufio.NewReader(conn) + for { + message, err := reader.ReadString('\n') + if err == io.EOF { //nolint:errorlint + break + } + noerr(err) + // fmt.Println("ECHO SERVER >>>", string(message)) // debug + _, err = conn.Write([]byte("echo: " + message + "\n")) + noerr(err) + } + } + }() + + return func() { + noerr(listener.Close()) + } +} + +func noerr(err error) { + if err != nil { + panic(err) + } +} diff --git a/main.go b/main.go index dbece6b..4c88925 100644 --- a/main.go +++ b/main.go @@ -1,58 +1,72 @@ package main import ( + "bytes" + "context" + "fmt" "io" + "log" "os" - "sync" + "path" + "runtime" + "runtime/debug" cli "github.com/urfave/cli/v2" - "golang.org/x/net/context" ) func main() { + ctx, cancel := context.WithCancel(context.Background()) // TODO: application context, good for graceful shutdown + defer cancel() + build, _ := debug.ReadBuildInfo() app := &cli.App{ + Version: build.Main.Version, + Usage: "Client and server parts to proxy SSH (TCP) over UDP using QUIC transport", Commands: []*cli.Command{ { Name: "server", Flags: []cli.Flag{ - &cli.StringFlag{Name: "bind", Value: "localhost:4242"}, + &cli.StringFlag{Name: "bind", Value: "localhost:4242", Usage: "bind address"}, + &cli.StringFlag{Name: "sshdaddr", Value: "localhost:22", Usage: "target address of sshd"}, }, Action: server, }, { Name: "client", Flags: []cli.Flag{ - &cli.StringFlag{Name: "addr", Value: "localhost:4242"}, + &cli.StringFlag{Name: "addr", Value: "localhost:4242", Usage: "address of server"}, + &cli.StringFlag{Name: "localaddr", Value: ":0", Usage: "source address of UDP packets"}, }, Action: client, }, }, } - if err := app.Run(os.Args); err != nil { - panic(err) + if err := app.RunContext(ctx, os.Args); err != nil { + logf(ctx, "Error: %v", err) } } -func readAndWrite(ctx context.Context, r io.Reader, w io.Writer, wg *sync.WaitGroup) <-chan error { +func readAndWrite(ctx context.Context, r io.Reader, w io.Writer) <-chan error { c := make(chan error) go func() { - if wg != nil { - defer wg.Done() - } - buff := make([]byte, 1024) + defer close(c) + + buff := make([]byte, 8*1024) for { select { case <-ctx.Done(): + c <- er(ctx, ctx.Err()) return default: nr, err := r.Read(buff) if err != nil { + c <- er(ctx, err) return } if nr > 0 { - _, err := w.Write(buff[:nr]) + _, err := io.Copy(w, bytes.NewReader(buff[:nr])) if err != nil { + c <- er(ctx, err) return } } @@ -61,3 +75,31 @@ func readAndWrite(ctx context.Context, r io.Reader, w io.Writer, wg *sync.WaitGr }() return c } + +func er(ctx context.Context, e error) error { + _, f, l, _ := runtime.Caller(1) + return fmt.Errorf("[%s] %s:%d: %w", label(ctx), path.Base(f), l, e) +} + +func logf(ctx context.Context, format string, v ...any) { + log.Printf("[%s] %s", label(ctx), fmt.Sprintf(format, v...)) +} + +type lableKeyT int + +const lableKey = lableKeyT(0) + +func withLabel(ctx context.Context, label string) context.Context { + if parent, ok := ctx.Value(lableKey).(string); ok { + label = parent + ">" + label + } + return context.WithValue(ctx, lableKey, label) +} + +func label(ctx context.Context) string { + label, _ := ctx.Value(lableKey).(string) + if label == "" { + return "main" + } + return label +} diff --git a/server.go b/server.go index 5cd926a..45cd3fa 100644 --- a/server.go +++ b/server.go @@ -1,112 +1,109 @@ package main import ( + "context" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "encoding/pem" "io" - "log" "math/big" "net" - "sync" quic "github.com/quic-go/quic-go" cli "github.com/urfave/cli/v2" - "golang.org/x/net/context" ) func server(c *cli.Context) error { + ctx := withLabel(c.Context, "server") + // generate TLS certificate - key, err := rsa.GenerateKey(rand.Reader, 1024) + key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - return err + return er(ctx, err) } template := x509.Certificate{SerialNumber: big.NewInt(1)} certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) if err != nil { - return err + return er(ctx, err) } keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { - return err + return er(ctx, err) } config := &tls.Config{ Certificates: []tls.Certificate{tlsCert}, NextProtos: []string{"quicssh"}, } + raddr, err := net.ResolveTCPAddr("tcp", c.String("sshdaddr")) + if err != nil { + return er(ctx, err) + } + // configure listener listener, err := quic.ListenAddr(c.String("bind"), config, nil) if err != nil { - return err + return er(ctx, err) } defer listener.Close() - log.Printf("Listening at %q...", c.String("bind")) + logf(ctx, "Listening at %q... (sshd addr: %q)", c.String("bind"), c.String("sshdaddr")) - ctx := context.Background() for { - log.Printf("Accepting connection...") + logf(ctx, "Accepting connection...") session, err := listener.Accept(ctx) if err != nil { - log.Printf("listener error: %v", err) + logf(ctx, "listener error: %v", err) continue } - go serverSessionHandler(ctx, session) + go serverSessionHandler(ctx, session, raddr) } } -func serverSessionHandler(ctx context.Context, session quic.Connection) { - log.Printf("hanling session...") +func serverSessionHandler(ctx context.Context, session quic.Connection, raddr *net.TCPAddr) { + logf(ctx, "Handling session...") defer func() { if err := session.CloseWithError(0, "close"); err != nil { - log.Printf("session close error: %v", err) + logf(ctx, "Session close error: %v", err) } }() for { stream, err := session.AcceptStream(ctx) if err != nil { - log.Printf("session error: %v", err) + logf(ctx, "Session error: %v", err) break } - go serverStreamHandler(ctx, stream) + go serverStreamHandler(ctx, stream, raddr) } } -func serverStreamHandler(ctx context.Context, conn io.ReadWriteCloser) { - log.Printf("handling stream...") +func serverStreamHandler(ctx context.Context, conn io.ReadWriteCloser, raddr *net.TCPAddr) { + logf(ctx, "Handling stream...") defer conn.Close() - rConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.IP{127, 0, 0, 1}, Port: 22}) + rConn, err := net.DialTCP("tcp", nil, raddr) if err != nil { - log.Printf("dial error: %v", err) + logf(ctx, "Dial error: %v", err) return } defer rConn.Close() ctx, cancel := context.WithCancel(ctx) + defer cancel() - var wg sync.WaitGroup - wg.Add(2) - c1 := readAndWrite(ctx, conn, rConn, &wg) - c2 := readAndWrite(ctx, rConn, conn, &wg) + c1 := readAndWrite(withLabel(ctx, "toSSHD"), conn, rConn) + c2 := readAndWrite(withLabel(ctx, "fromSSHD"), rConn, conn) select { case err = <-c1: - if err != nil { - log.Printf("readAndWrite error on c1: %v", err) - return - } case err = <-c2: - if err != nil { - log.Printf("readAndWrite error on c2: %v", err) - return - } } - cancel() - wg.Wait() - log.Printf("Piping finished") + if err != nil { + logf(ctx, "readAndWrite error: %v", err) + return + } + logf(ctx, "Piping finished") }