From e66f78b6f251eafb440f02db8092183620c635cc Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sun, 16 Mar 2025 08:03:53 +0300 Subject: [PATCH 01/13] Updating deps, adding more options, updating readme, fixing bugs and typos --- .golangci.yml | 28 +++++++--------- README.md | 38 +++++++++++---------- client.go | 38 ++++++++++++--------- go.mod | 29 ++++++++-------- go.sum | 91 +++++++++++++++++++++------------------------------ main.go | 26 ++++++++++----- server.go | 49 +++++++++++++-------------- 7 files changed, 148 insertions(+), 151 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index cf01fa4..da0cc81 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,19 +16,15 @@ linters-settings: locale: US 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 + - gosec # must be turned on + - lll # nice to have + - mnd # nice to have + - nlreturn # nice to have + - tenv + - varnamelen + - wrapcheck # nice to have + - 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..3800f1e 100644 --- a/client.go +++ b/client.go @@ -3,8 +3,9 @@ package main import ( "crypto/tls" "log" + "net" "os" - "sync" + "time" quic "github.com/quic-go/quic-go" cli "github.com/urfave/cli/v2" @@ -13,14 +14,28 @@ import ( func client(c *cli.Context) error { ctx, cancel := context.WithCancel(context.Background()) + defer cancel() 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 + } + srcAddr, err := net.ResolveUDPAddr("udp", c.String("localaddr")) + if err != nil { + return err + } + + log.Printf("Dialing %q->%q...", srcAddr.String(), udpAddr.String()) + conn, err := net.ListenUDP("udp", srcAddr) + if err != nil { + return err + } + session, err := quic.Dial(ctx, conn, udpAddr, config, &quic.Config{MaxIdleTimeout: 10 * time.Second, KeepAlivePeriod: 5 * time.Second}) if err != nil { return err } @@ -37,21 +52,14 @@ func client(c *cli.Context) error { } 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) + c1 := readAndWrite(ctx, stream, os.Stdout) + c2 := readAndWrite(ctx, os.Stdin, stream) 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/main.go b/main.go index dbece6b..b10c60f 100644 --- a/main.go +++ b/main.go @@ -1,28 +1,34 @@ package main import ( + "bytes" "io" "os" - "sync" + "runtime/debug" cli "github.com/urfave/cli/v2" "golang.org/x/net/context" ) func main() { + 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, }, @@ -33,26 +39,28 @@ func main() { } } -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 <- ctx.Err() return default: nr, err := r.Read(buff) if err != nil { + c <- err return } if nr > 0 { - _, err := w.Write(buff[:nr]) + _, err := io.Copy(w, bytes.NewReader(buff[:nr])) if err != nil { + c <- err return } } diff --git a/server.go b/server.go index 5cd926a..b642dd6 100644 --- a/server.go +++ b/server.go @@ -10,7 +10,6 @@ import ( "log" "math/big" "net" - "sync" quic "github.com/quic-go/quic-go" cli "github.com/urfave/cli/v2" @@ -39,13 +38,18 @@ func server(c *cli.Context) error { NextProtos: []string{"quicssh"}, } + raddr, err := net.ResolveTCPAddr("tcp", c.String("sshdaddr")) + if err != nil { + return err + } + // configure listener listener, err := quic.ListenAddr(c.String("bind"), config, nil) if err != nil { return err } defer listener.Close() - log.Printf("Listening at %q...", c.String("bind")) + log.Printf("Listening at %q... (sshd addr: %q)", c.String("bind"), c.String("sshdaddr")) ctx := context.Background() for { @@ -56,57 +60,50 @@ func server(c *cli.Context) error { 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) { + log.Printf("Hanling session...") defer func() { if err := session.CloseWithError(0, "close"); err != nil { - log.Printf("session close error: %v", err) + log.Printf("Session close error: %v", err) } }() for { stream, err := session.AcceptStream(ctx) if err != nil { - log.Printf("session error: %v", err) + log.Printf("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) { + log.Printf("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) + log.Printf("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(ctx, conn, rConn) + c2 := readAndWrite(ctx, 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() + if err != nil { + log.Printf("readAndWrite error: %v", err) + return + } log.Printf("Piping finished") } From 95eaec3e14b56fa4ae9da01a0623a5eaf18e3e3e Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sat, 22 Mar 2025 04:33:02 +0300 Subject: [PATCH 02/13] Client: closing stream --- client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client.go b/client.go index 3800f1e..2895e97 100644 --- a/client.go +++ b/client.go @@ -50,6 +50,7 @@ func client(c *cli.Context) error { if err != nil { return err } + defer stream.Close() log.Printf("Piping stream with QUIC...") c1 := readAndWrite(ctx, stream, os.Stdout) From 5e3c05a6c656f9bf6006962221363bc715d574c8 Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sat, 22 Mar 2025 04:47:42 +0300 Subject: [PATCH 03/13] Github action: lint --- .github/workflows/lint.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/lint.yaml 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" From 41c9f4f64215c5919b04b5b7d28b589acd0f9eea Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sat, 22 Mar 2025 04:54:20 +0300 Subject: [PATCH 04/13] Github action: lint: golangci.yml: golangci-lint config verify --- .golangci.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index da0cc81..27d6a98 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,14 +1,4 @@ -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 From 0a6ce951dbe18823351e5f6b2354f334c8fc4b49 Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sat, 22 Mar 2025 04:56:48 +0300 Subject: [PATCH 05/13] Github action: build --- .github/workflows/build.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/build.yaml 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 ." From 21008259633e9bcfde0fd16db0773f85b525a79c Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sat, 22 Mar 2025 07:14:59 +0300 Subject: [PATCH 06/13] CI: test --- .github/workflows/test.yaml | 21 ++++++++ .golangci.yml | 1 + client.go | 10 +++- integration_test.go | 104 ++++++++++++++++++++++++++++++++++++ 4 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test.yaml create mode 100644 integration_test.go diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..164067e --- /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 ." diff --git a/.golangci.yml b/.golangci.yml index 27d6a98..54e7261 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,3 +18,4 @@ linters: - varnamelen - wrapcheck # nice to have - wsl + - paralleltest # for now we have only one integration (not parallelable) test diff --git a/client.go b/client.go index 2895e97..ebf0e40 100644 --- a/client.go +++ b/client.go @@ -2,6 +2,7 @@ package main import ( "crypto/tls" + "io" "log" "net" "os" @@ -12,6 +13,11 @@ import ( "golang.org/x/net/context" ) +var ( // the simplest way to make code testable + inputStream = io.Reader(os.Stdin) //nolint:gochecknoglobals + outputStream = io.Writer(os.Stdout) //nolint:gochecknoglobals +) + func client(c *cli.Context) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -53,8 +59,8 @@ func client(c *cli.Context) error { defer stream.Close() log.Printf("Piping stream with QUIC...") - c1 := readAndWrite(ctx, stream, os.Stdout) - c2 := readAndWrite(ctx, os.Stdin, stream) + c1 := readAndWrite(ctx, stream, outputStream) + c2 := readAndWrite(ctx, inputStream, stream) select { case err = <-c1: case err = <-c2: diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..af97185 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,104 @@ +package main + +import ( + "bufio" + "flag" + "io" + "net" + "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() { + server(serverContext) //nolint:errcheck // TODO: make it able to shutdown gracefully, check error + }() + + // run quicssh client + clientContext := flags("addr", bindAddr, "localaddr", ":0") + wr, rd := tweaksStdIO() + go func() { + client(clientContext) //nolint:errcheck // TODO: shutdownable, check 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() (io.Writer, io.Reader) { + r1, w1 := io.Pipe() + r2, w2 := io.Pipe() + inputStream = r1 // ATTENTION: tweaking global variable + outputStream = w2 // ATTENTION: tweaking global variable + 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) + } +} From 22ed8f04e9b682adeef9739a71523d529e4b1b01 Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sat, 22 Mar 2025 15:40:32 +0300 Subject: [PATCH 07/13] Security: fixes and linting --- .golangci.yml | 3 ++- server.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 54e7261..9eec8ac 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,13 +4,14 @@ linters-settings: min-occurrences: 4 misspell: locale: US + gosec: + excludes: [G402] linters: enable-all: true disable: - depguard - exhaustruct - - gosec # must be turned on - lll # nice to have - mnd # nice to have - nlreturn # nice to have diff --git a/server.go b/server.go index b642dd6..9dd7dc6 100644 --- a/server.go +++ b/server.go @@ -18,7 +18,7 @@ import ( func server(c *cli.Context) error { // generate TLS certificate - key, err := rsa.GenerateKey(rand.Reader, 1024) + key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return err } From 33cd3ca4c4fe84669c1d50b4e3355cc5594b3d41 Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sat, 22 Mar 2025 15:57:33 +0300 Subject: [PATCH 08/13] Cosmetics: just line length --- .golangci.yml | 1 - client.go | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 9eec8ac..0cfdb39 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,7 +12,6 @@ linters: disable: - depguard - exhaustruct - - lll # nice to have - mnd # nice to have - nlreturn # nice to have - tenv diff --git a/client.go b/client.go index ebf0e40..8efcb04 100644 --- a/client.go +++ b/client.go @@ -41,7 +41,8 @@ func client(c *cli.Context) error { if err != nil { return err } - session, err := quic.Dial(ctx, conn, udpAddr, config, &quic.Config{MaxIdleTimeout: 10 * time.Second, KeepAlivePeriod: 5 * time.Second}) + quicConfig := &quic.Config{MaxIdleTimeout: 10 * time.Second, KeepAlivePeriod: 5 * time.Second} + session, err := quic.Dial(ctx, conn, udpAddr, config, quicConfig) if err != nil { return err } From b8c046639f8a0940af275beec409ec9bbdfe9b1e Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sun, 23 Mar 2025 07:45:47 +0300 Subject: [PATCH 09/13] CI: tests: error handling, io mocking using urfave/cli, timeout --- .github/workflows/test.yaml | 2 +- .golangci.yml | 3 ++- client.go | 11 ++--------- integration_test.go | 22 +++++++++++++++------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 164067e..1655fff 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -18,4 +18,4 @@ jobs: - uses: actions/setup-go@v5 with: go-version: "${{ matrix.go }}" - - run: "go test -v -cover ." + - run: "go test -v -cover -timeout=5s ." diff --git a/.golangci.yml b/.golangci.yml index 0cfdb39..4ce99da 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,10 +12,11 @@ linters: 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 - wrapcheck # nice to have - wsl - - paralleltest # for now we have only one integration (not parallelable) test diff --git a/client.go b/client.go index 8efcb04..539a7fe 100644 --- a/client.go +++ b/client.go @@ -2,10 +2,8 @@ package main import ( "crypto/tls" - "io" "log" "net" - "os" "time" quic "github.com/quic-go/quic-go" @@ -13,11 +11,6 @@ import ( "golang.org/x/net/context" ) -var ( // the simplest way to make code testable - inputStream = io.Reader(os.Stdin) //nolint:gochecknoglobals - outputStream = io.Writer(os.Stdout) //nolint:gochecknoglobals -) - func client(c *cli.Context) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -60,8 +53,8 @@ func client(c *cli.Context) error { defer stream.Close() log.Printf("Piping stream with QUIC...") - c1 := readAndWrite(ctx, stream, outputStream) - c2 := readAndWrite(ctx, inputStream, stream) + c1 := readAndWrite(ctx, stream, c.App.Writer) // App.Writer is stdout + c2 := readAndWrite(ctx, c.App.Reader, stream) // App.Reader is stdin select { case err = <-c1: case err = <-c2: diff --git a/integration_test.go b/integration_test.go index af97185..750ead3 100644 --- a/integration_test.go +++ b/integration_test.go @@ -2,9 +2,11 @@ package main import ( "bufio" + "errors" "flag" "io" "net" + "strings" "testing" cli "github.com/urfave/cli/v2" @@ -23,14 +25,20 @@ func TestInegratinGoldenFlow(t *testing.T) { // run quicssh server serverContext := flags("sshdaddr", sshdAddr, "bind", bindAddr) go func() { - server(serverContext) //nolint:errcheck // TODO: make it able to shutdown gracefully, check error + 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() + wr, rd := tweaksStdIO(clientContext) go func() { - client(clientContext) //nolint:errcheck // TODO: shutdownable, check error + err := client(clientContext) // TODO: shutdownable, check error + if !errors.Is(err, io.EOF) { + panic(err.Error()) + } }() // writing data to client @@ -49,12 +57,12 @@ func TestInegratinGoldenFlow(t *testing.T) { } } -func tweaksStdIO() (io.Writer, io.Reader) { +func tweaksStdIO(c *cli.Context) (io.Writer, io.Reader) { r1, w1 := io.Pipe() r2, w2 := io.Pipe() - inputStream = r1 // ATTENTION: tweaking global variable - outputStream = w2 // ATTENTION: tweaking global variable - return w1, r2 // returning the opposite end of pipes + c.App.Reader = r1 + c.App.Writer = w2 + return w1, r2 // returning the opposite end of pipes } func flags(pairs ...string) *cli.Context { From aca2dafe5df0253bc2a46d8d3e031e121aba7b4f Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sat, 22 Mar 2025 04:34:43 +0300 Subject: [PATCH 10/13] Error wrapping --- .golangci.yml | 1 - client.go | 10 +++++----- main.go | 17 +++++++++++++---- server.go | 10 +++++----- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 4ce99da..958a292 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,5 +18,4 @@ linters: - paralleltest # for now we have only one integration (not parallelable) test - tenv - varnamelen - - wrapcheck # nice to have - wsl diff --git a/client.go b/client.go index 539a7fe..5a28f49 100644 --- a/client.go +++ b/client.go @@ -22,22 +22,22 @@ func client(c *cli.Context) error { udpAddr, err := net.ResolveUDPAddr("udp", c.String("addr")) if err != nil { - return err + return er(err) } srcAddr, err := net.ResolveUDPAddr("udp", c.String("localaddr")) if err != nil { - return err + return er(err) } log.Printf("Dialing %q->%q...", srcAddr.String(), udpAddr.String()) conn, err := net.ListenUDP("udp", srcAddr) if err != nil { - return err + return er(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 err + return er(err) } defer func() { if err := session.CloseWithError(0, "close"); err != nil { @@ -48,7 +48,7 @@ func client(c *cli.Context) error { log.Printf("Opening stream sync...") stream, err := session.OpenStreamSync(ctx) if err != nil { - return err + return er(err) } defer stream.Close() diff --git a/main.go b/main.go index b10c60f..f1c57ac 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,12 @@ package main import ( "bytes" + "fmt" "io" + "log" "os" + "path" + "runtime" "runtime/debug" cli "github.com/urfave/cli/v2" @@ -35,7 +39,7 @@ func main() { }, } if err := app.Run(os.Args); err != nil { - panic(err) + log.Printf("Error: %v", err) } } @@ -49,18 +53,18 @@ func readAndWrite(ctx context.Context, r io.Reader, w io.Writer) <-chan error { for { select { case <-ctx.Done(): - c <- ctx.Err() + c <- er(ctx.Err()) return default: nr, err := r.Read(buff) if err != nil { - c <- err + c <- er(err) return } if nr > 0 { _, err := io.Copy(w, bytes.NewReader(buff[:nr])) if err != nil { - c <- err + c <- er(err) return } } @@ -69,3 +73,8 @@ func readAndWrite(ctx context.Context, r io.Reader, w io.Writer) <-chan error { }() return c } + +func er(e error) error { + _, f, l, _ := runtime.Caller(1) + return fmt.Errorf("%s:%d: %w", path.Base(f), l, e) +} diff --git a/server.go b/server.go index 9dd7dc6..f7d17c1 100644 --- a/server.go +++ b/server.go @@ -20,18 +20,18 @@ func server(c *cli.Context) error { // generate TLS certificate key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - return err + return er(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(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(err) } config := &tls.Config{ Certificates: []tls.Certificate{tlsCert}, @@ -40,13 +40,13 @@ func server(c *cli.Context) error { raddr, err := net.ResolveTCPAddr("tcp", c.String("sshdaddr")) if err != nil { - return err + return er(err) } // configure listener listener, err := quic.ListenAddr(c.String("bind"), config, nil) if err != nil { - return err + return er(err) } defer listener.Close() log.Printf("Listening at %q... (sshd addr: %q)", c.String("bind"), c.String("sshdaddr")) From 678b2de3a286805118ce578ae55dc6a431af5d81 Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sun, 23 Mar 2025 07:48:30 +0300 Subject: [PATCH 11/13] Use ctx for logging and error wrapping --- client.go | 25 ++++++++++++------------- main.go | 36 ++++++++++++++++++++++++++++++------ server.go | 38 +++++++++++++++++++------------------- 3 files changed, 61 insertions(+), 38 deletions(-) diff --git a/client.go b/client.go index 5a28f49..ba85abd 100644 --- a/client.go +++ b/client.go @@ -2,7 +2,6 @@ package main import ( "crypto/tls" - "log" "net" "time" @@ -12,7 +11,7 @@ import ( ) func client(c *cli.Context) error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(withLabel(context.Background(), "client")) defer cancel() config := &tls.Config{ @@ -22,39 +21,39 @@ func client(c *cli.Context) error { udpAddr, err := net.ResolveUDPAddr("udp", c.String("addr")) if err != nil { - return er(err) + return er(ctx, err) } srcAddr, err := net.ResolveUDPAddr("udp", c.String("localaddr")) if err != nil { - return er(err) + return er(ctx, err) } - log.Printf("Dialing %q->%q...", srcAddr.String(), udpAddr.String()) + logf(ctx, "Dialing %q->%q...", srcAddr.String(), udpAddr.String()) conn, err := net.ListenUDP("udp", srcAddr) if err != nil { - return er(err) + 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(err) + 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 er(err) + return er(ctx, err) } defer stream.Close() - log.Printf("Piping stream with QUIC...") - c1 := readAndWrite(ctx, stream, c.App.Writer) // App.Writer is stdout - c2 := readAndWrite(ctx, c.App.Reader, stream) // App.Reader is stdin + 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: case err = <-c2: diff --git a/main.go b/main.go index f1c57ac..7332cc8 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( ) func main() { + ctx := context.Background() // TODO: it is application context, could be used for graceful shutdown build, _ := debug.ReadBuildInfo() app := &cli.App{ Version: build.Main.Version, @@ -39,7 +40,7 @@ func main() { }, } if err := app.Run(os.Args); err != nil { - log.Printf("Error: %v", err) + logf(ctx, "Error: %v", err) } } @@ -53,18 +54,18 @@ func readAndWrite(ctx context.Context, r io.Reader, w io.Writer) <-chan error { for { select { case <-ctx.Done(): - c <- er(ctx.Err()) + c <- er(ctx, ctx.Err()) return default: nr, err := r.Read(buff) if err != nil { - c <- er(err) + c <- er(ctx, err) return } if nr > 0 { _, err := io.Copy(w, bytes.NewReader(buff[:nr])) if err != nil { - c <- er(err) + c <- er(ctx, err) return } } @@ -74,7 +75,30 @@ func readAndWrite(ctx context.Context, r io.Reader, w io.Writer) <-chan error { return c } -func er(e error) error { +func er(ctx context.Context, e error) error { _, f, l, _ := runtime.Caller(1) - return fmt.Errorf("%s:%d: %w", path.Base(f), l, e) + 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 f7d17c1..bdf199f 100644 --- a/server.go +++ b/server.go @@ -7,7 +7,6 @@ import ( "crypto/x509" "encoding/pem" "io" - "log" "math/big" "net" @@ -17,21 +16,23 @@ import ( ) func server(c *cli.Context) error { + ctx := withLabel(context.Background(), "server") + // generate TLS certificate key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - return er(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 er(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 er(err) + return er(ctx, err) } config := &tls.Config{ Certificates: []tls.Certificate{tlsCert}, @@ -40,23 +41,22 @@ func server(c *cli.Context) error { raddr, err := net.ResolveTCPAddr("tcp", c.String("sshdaddr")) if err != nil { - return er(err) + return er(ctx, err) } // configure listener listener, err := quic.ListenAddr(c.String("bind"), config, nil) if err != nil { - return er(err) + return er(ctx, err) } defer listener.Close() - log.Printf("Listening at %q... (sshd addr: %q)", c.String("bind"), c.String("sshdaddr")) + 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 } @@ -65,16 +65,16 @@ func server(c *cli.Context) error { } func serverSessionHandler(ctx context.Context, session quic.Connection, raddr *net.TCPAddr) { - log.Printf("Hanling session...") + logf(ctx, "Hanling 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, raddr) @@ -82,12 +82,12 @@ func serverSessionHandler(ctx context.Context, session quic.Connection, raddr *n } func serverStreamHandler(ctx context.Context, conn io.ReadWriteCloser, raddr *net.TCPAddr) { - log.Printf("Handling stream...") + logf(ctx, "Handling stream...") defer conn.Close() 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() @@ -95,15 +95,15 @@ func serverStreamHandler(ctx context.Context, conn io.ReadWriteCloser, raddr *ne ctx, cancel := context.WithCancel(ctx) defer cancel() - c1 := readAndWrite(ctx, conn, rConn) - c2 := readAndWrite(ctx, rConn, conn) + c1 := readAndWrite(withLabel(ctx, "toSSHD"), conn, rConn) + c2 := readAndWrite(withLabel(ctx, "fromSSHD"), rConn, conn) select { case err = <-c1: case err = <-c2: } if err != nil { - log.Printf("readAndWrite error: %v", err) + logf(ctx, "readAndWrite error: %v", err) return } - log.Printf("Piping finished") + logf(ctx, "Piping finished") } From fc6300ce318019e3ec766e9812b9b765df2724b5 Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sun, 23 Mar 2025 08:10:48 +0300 Subject: [PATCH 12/13] Global context, cosmetics --- .golangci.yml | 5 +++++ client.go | 4 +--- main.go | 7 ++++--- server.go | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 958a292..8603c5a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -6,6 +6,11 @@ linters-settings: locale: US gosec: excludes: [G402] + ireturn: + allow: + - context.Context + - error + - io.(Reader|Writer) linters: enable-all: true diff --git a/client.go b/client.go index ba85abd..beee3e5 100644 --- a/client.go +++ b/client.go @@ -7,12 +7,10 @@ import ( 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(withLabel(context.Background(), "client")) - defer cancel() + ctx := withLabel(c.Context, "client") config := &tls.Config{ InsecureSkipVerify: true, diff --git a/main.go b/main.go index 7332cc8..4c88925 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "fmt" "io" "log" @@ -11,11 +12,11 @@ import ( "runtime/debug" cli "github.com/urfave/cli/v2" - "golang.org/x/net/context" ) func main() { - ctx := context.Background() // TODO: it is application context, could be used for graceful shutdown + 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, @@ -39,7 +40,7 @@ func main() { }, }, } - if err := app.Run(os.Args); err != nil { + if err := app.RunContext(ctx, os.Args); err != nil { logf(ctx, "Error: %v", err) } } diff --git a/server.go b/server.go index bdf199f..5b1eaf2 100644 --- a/server.go +++ b/server.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/rand" "crypto/rsa" "crypto/tls" @@ -12,11 +13,10 @@ import ( 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(context.Background(), "server") + ctx := withLabel(c.Context, "server") // generate TLS certificate key, err := rsa.GenerateKey(rand.Reader, 2048) From df2853eb5a80771947d36d3e5cf8b3a180961c94 Mon Sep 17 00:00:00 2001 From: Alexey Michurin Date: Sat, 29 Mar 2025 06:56:09 +0300 Subject: [PATCH 13/13] Typo --- server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.go b/server.go index 9dd7dc6..f0f7575 100644 --- a/server.go +++ b/server.go @@ -65,7 +65,7 @@ func server(c *cli.Context) error { } func serverSessionHandler(ctx context.Context, session quic.Connection, raddr *net.TCPAddr) { - log.Printf("Hanling session...") + log.Printf("Handling session...") defer func() { if err := session.CloseWithError(0, "close"); err != nil { log.Printf("Session close error: %v", err)