Skip to content

Commit cc2c521

Browse files
committed
feat: POC
1 parent 59fab45 commit cc2c521

File tree

7 files changed

+341
-4
lines changed

7 files changed

+341
-4
lines changed

client.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package main
2+
3+
import (
4+
"crypto/tls"
5+
"log"
6+
"os"
7+
"sync"
8+
9+
quic "github.com/lucas-clemente/quic-go"
10+
"golang.org/x/net/context"
11+
cli "gopkg.in/urfave/cli.v2"
12+
)
13+
14+
func client(c *cli.Context) error {
15+
ctx, cancel := context.WithCancel(context.Background())
16+
17+
config := &tls.Config{
18+
InsecureSkipVerify: true,
19+
NextProtos: []string{"quicssh"},
20+
}
21+
22+
log.Printf("Dialing %q...", c.String("addr"))
23+
session, err := quic.DialAddr(c.String("addr"), config, nil)
24+
if err != nil {
25+
return err
26+
}
27+
defer session.Close()
28+
29+
log.Printf("Opening stream sync...")
30+
stream, err := session.OpenStreamSync(ctx)
31+
if err != nil {
32+
return err
33+
}
34+
35+
log.Printf("Piping stream with QUIC...")
36+
var wg sync.WaitGroup
37+
wg.Add(3)
38+
c1 := readAndWrite(ctx, stream, os.Stdout, &wg)
39+
c2 := readAndWrite(ctx, os.Stdin, stream, &wg)
40+
select {
41+
case err = <-c1:
42+
if err != nil {
43+
return err
44+
}
45+
case err = <-c2:
46+
if err != nil {
47+
return err
48+
}
49+
}
50+
cancel()
51+
wg.Wait()
52+
return nil
53+
}

go.mod

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1-
module moul.io/golang-repo-template
1+
module moul.io/quicssh
22

33
go 1.12
4+
5+
require (
6+
github.com/lucas-clemente/quic-go v0.7.1-0.20190710050138-1441923ab031
7+
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7
8+
gopkg.in/urfave/cli.v2 v2.0.0-20180128182452-d3ae77c26ac8
9+
)

go.sum

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
2+
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
3+
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
4+
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
5+
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
6+
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
7+
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
8+
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
9+
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
10+
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
11+
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
12+
github.com/lucas-clemente/quic-go v0.7.1-0.20190710050138-1441923ab031 h1:wjcGvgllMOQw8wNYFH6acq/KlTAdjKMSo1EUYybHXto=
13+
github.com/lucas-clemente/quic-go v0.7.1-0.20190710050138-1441923ab031/go.mod h1:lb5aAxL68VvhZ00e7yYuQVK/9FLggtYy4qo7oI5qzqA=
14+
github.com/marten-seemann/qpack v0.1.0 h1:/0M7lkda/6mus9B8u34Asqm8ZhHAAt9Ho0vniNuVSVg=
15+
github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI=
16+
github.com/marten-seemann/qtls v0.3.1 h1:ySYIvhFjFY2JsNHY6VACvomMEDy3EvdPA6yciUFAiHw=
17+
github.com/marten-seemann/qtls v0.3.1/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
18+
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
19+
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
20+
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
21+
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
22+
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
23+
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU=
24+
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
25+
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
26+
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7 h1:Qe/u+eY379X4He4GBMFZYu3pmh1ML5yT1aL1ndNM1zQ=
27+
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
28+
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
29+
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
30+
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
31+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
32+
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e h1:ZytStCyV048ZqDsWHiYDdoI2Vd4msMcrDECFxS+tL9c=
33+
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
34+
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
35+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
36+
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
37+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
38+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
39+
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
40+
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
41+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
42+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
43+
gopkg.in/urfave/cli.v2 v2.0.0-20180128182452-d3ae77c26ac8 h1:Ggy3mWN4l3PUFPfSG0YB3n5fVYggzysUmiUQ89SnX6Y=
44+
gopkg.in/urfave/cli.v2 v2.0.0-20180128182452-d3ae77c26ac8/go.mod h1:cKXr3E0k4aosgycml1b5z33BVV6hai1Kh7uDgFOkbcs=
45+
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
46+
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

main.go

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,63 @@
1-
package main // import "moul.io/golang-repo-template"
1+
package main
22

3-
import "fmt"
3+
import (
4+
"io"
5+
"os"
6+
"sync"
7+
8+
"golang.org/x/net/context"
9+
cli "gopkg.in/urfave/cli.v2"
10+
)
411

512
func main() {
6-
fmt.Println("Hello World!")
13+
app := &cli.App{
14+
Commands: []*cli.Command{
15+
{
16+
Name: "server",
17+
Flags: []cli.Flag{
18+
&cli.StringFlag{Name: "bind", Value: "localhost:4242"},
19+
},
20+
Action: server,
21+
},
22+
{
23+
Name: "client",
24+
Flags: []cli.Flag{
25+
&cli.StringFlag{Name: "addr", Value: "localhost:4242"},
26+
},
27+
Action: client,
28+
},
29+
},
30+
}
31+
if err := app.Run(os.Args); err != nil {
32+
panic(err)
33+
}
34+
}
35+
36+
func readAndWrite(ctx context.Context, r io.Reader, w io.Writer, wg *sync.WaitGroup) <-chan error {
37+
c := make(chan error)
38+
go func() {
39+
if wg != nil {
40+
defer wg.Done()
41+
}
42+
buff := make([]byte, 1024)
43+
44+
for {
45+
select {
46+
case <-ctx.Done():
47+
return
48+
default:
49+
nr, err := r.Read(buff)
50+
if err != nil {
51+
return
52+
}
53+
if nr > 0 {
54+
_, err := w.Write(buff[:nr])
55+
if err != nil {
56+
return
57+
}
58+
}
59+
}
60+
}
61+
}()
62+
return c
763
}

server.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package main
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/rsa"
6+
"crypto/tls"
7+
"crypto/x509"
8+
"encoding/pem"
9+
"io"
10+
"log"
11+
"math/big"
12+
"net"
13+
"sync"
14+
15+
quic "github.com/lucas-clemente/quic-go"
16+
"golang.org/x/net/context"
17+
cli "gopkg.in/urfave/cli.v2"
18+
)
19+
20+
func server(c *cli.Context) error {
21+
// generate TLS certificate
22+
key, err := rsa.GenerateKey(rand.Reader, 1024)
23+
if err != nil {
24+
return err
25+
}
26+
template := x509.Certificate{SerialNumber: big.NewInt(1)}
27+
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
28+
if err != nil {
29+
return err
30+
}
31+
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
32+
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
33+
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
34+
if err != nil {
35+
return err
36+
}
37+
config := &tls.Config{
38+
Certificates: []tls.Certificate{tlsCert},
39+
NextProtos: []string{"quicssh"},
40+
}
41+
42+
// configure listener
43+
listener, err := quic.ListenAddr(c.String("bind"), config, nil)
44+
if err != nil {
45+
return err
46+
}
47+
defer listener.Close()
48+
log.Printf("Listening at %q...", c.String("bind"))
49+
50+
ctx := context.Background()
51+
for {
52+
log.Printf("Accepting connection...")
53+
session, err := listener.Accept(ctx)
54+
if err != nil {
55+
log.Printf("listener error: %v", err)
56+
continue
57+
}
58+
59+
go serverSessionHandler(ctx, session)
60+
}
61+
return nil
62+
}
63+
64+
func serverSessionHandler(ctx context.Context, session quic.Session) {
65+
log.Printf("hanling session...")
66+
defer session.Close()
67+
for {
68+
stream, err := session.AcceptStream(ctx)
69+
if err != nil {
70+
log.Printf("session error: %v", err)
71+
break
72+
}
73+
go serverStreamHandler(ctx, stream)
74+
}
75+
}
76+
77+
func serverStreamHandler(ctx context.Context, conn io.ReadWriteCloser) {
78+
log.Printf("handling stream...")
79+
defer conn.Close()
80+
81+
rConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.IP{127, 0, 0, 1}, Port: 22})
82+
if err != nil {
83+
log.Printf("dial error: %v", err)
84+
return
85+
}
86+
defer rConn.Close()
87+
88+
ctx, cancel := context.WithCancel(ctx)
89+
90+
var wg sync.WaitGroup
91+
wg.Add(2)
92+
c1 := readAndWrite(ctx, conn, rConn, &wg)
93+
c2 := readAndWrite(ctx, rConn, conn, &wg)
94+
select {
95+
case err = <-c1:
96+
if err != nil {
97+
log.Printf("readAndWrite error on c1: %v", err)
98+
return
99+
}
100+
case err = <-c2:
101+
if err != nil {
102+
log.Printf("readAndWrite error on c2: %v", err)
103+
return
104+
}
105+
}
106+
cancel()
107+
wg.Wait()
108+
log.Printf("Piping finished")
109+
}
110+
111+
func netCopy(input io.Reader, output io.Writer) (err error) {
112+
buf := make([]byte, 8192)
113+
for {
114+
count, err := input.Read(buf)
115+
if err != nil {
116+
if err == io.EOF && count > 0 {
117+
output.Write(buf[:count])
118+
}
119+
break
120+
}
121+
if count > 0 {
122+
log.Println(buf, count)
123+
output.Write(buf[:count])
124+
}
125+
}
126+
return
127+
}

test.crt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDXTCCAkWgAwIBAgIJAIIvbSaJPzA1MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
3+
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
4+
aWRnaXRzIFB0eSBMdGQwHhcNMTkwNzEzMjE1MzMxWhcNMjAwNzEyMjE1MzMxWjBF
5+
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
6+
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
7+
CgKCAQEAmbucNcxAnXxFPT64uY1lKenrphuDg1MLrmvghoTRq+eVJH6QM58z+V3d
8+
2xbaPoIuFBMYPQP17oKHFl5V/tmZjqRKY9yg+JH71A2lmIu99vVYUiubzVnZX1QZ
9+
d+csr36B/R2fFCUONvb+BiCm65ArNd9pTaNRc2rxHbRa9nCTK5AT97DVgfuk9N+l
10+
SebekMQma/qD4f7VgDhi5FQV5JNen0WdT0/V2KJkMv/yKnvnnXiK3jRtCDSu/nj4
11+
CsMrLUffmpPn8FE5UImyDGYgor0V4N1mqo4NUWuJkaAxHvI5sT0+f9rm/8gnr/dX
12+
GnIvPRA33gwhYmAcNavqbf8N0xl/jwIDAQABo1AwTjAdBgNVHQ4EFgQUIP0H61kW
13+
YT7pBmwwLiVDImggl7owHwYDVR0jBBgwFoAUIP0H61kWYT7pBmwwLiVDImggl7ow
14+
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAT7KVlI90mpToEEQGfgO7
15+
MUUvllsuoYvzKDpic8svWgM1v3KKMwquD19sH+x8RwRZ6SVY+zSqn12VEL9W3nNa
16+
bTYtmtGwfkYfvIM3LldbTm15228YBEvDQUY+QAdJs/E9TpA4T/sOjz0COjOFWzso
17+
WS6x2J5I/d0POBRwDdASnZDCfSui/xUQxbucw28eITMyknZgvvVN6dejDjZSrZfw
18+
9nk93KQml4IUNWnfhrVtL1dUX1MyhBOIBmdPq33pe2GlyxqczSyn3+1H36wiPj4l
19+
wd7ZCyv1RiW6CaxtewSY4ct+NqunZ66cOBS/0aw5VxyuIVUAp5DtKVT2f35h/5+x
20+
iQ==
21+
-----END CERTIFICATE-----

test.key

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCZu5w1zECdfEU9
3+
Pri5jWUp6eumG4ODUwuua+CGhNGr55UkfpAznzP5Xd3bFto+gi4UExg9A/XugocW
4+
XlX+2ZmOpEpj3KD4kfvUDaWYi7329VhSK5vNWdlfVBl35yyvfoH9HZ8UJQ429v4G
5+
IKbrkCs132lNo1FzavEdtFr2cJMrkBP3sNWB+6T036VJ5t6QxCZr+oPh/tWAOGLk
6+
VBXkk16fRZ1PT9XYomQy//Iqe+edeIreNG0INK7+ePgKwystR9+ak+fwUTlQibIM
7+
ZiCivRXg3Waqjg1Ra4mRoDEe8jmxPT5/2ub/yCev91caci89EDfeDCFiYBw1q+pt
8+
/w3TGX+PAgMBAAECggEAA+pANyqFdr1EciPXxnnwWpnnc2p99ek2gfGjXSmiwVL7
9+
fFtwxq/GPhKC5OJ3GmJsU/yMgHlKWRGf6RTr8bqO65AJiPOEcfAdzq+uSO0+IDzt
10+
S+JqbFdebswQffo4LBv3qX+InpW2//VYUMWiGpuoTg3re5uuJldR3qTKMD57sP9H
11+
NzIPYRxaESn/owPGzp+MfjhGwx5yAmyQeorNaMydU7sKX/9uTmRLW82Uh+SV8u2n
12+
TswwR/iwHAPfvNr1rI2EiUBFDHlL1NIXDGKnMqqQFk/Y7kQkB47NA3n2IbPVkncL
13+
cJfMimOC7U/7k2m6J8J9aWOBfIf0S5A7EaJAe0jqGQKBgQDL7fB4/7dSEnqHR2qi
14+
3LLNk9y6l7XTLtYXkPiNcpxseT1/n8IK/hhIcQL7a16abVU4/0GFRpYOj1AEt6IB
15+
EnqM+DsGz4ho4X2ucOFJ80Af9UUUbSmtJowzSU+DXf+d3U7PcNf7vzXJmyUbWmGZ
16+
lpLXPTtsZTLum3U+nYkdj6trKwKBgQDA/ISbN5wIadL4vCvi/EHzzH6TPOip1arc
17+
QiKibaqKaDzdQhELJjg9Lkma8xgcVSovmMaQm+RATqUTuwSNufyNQ0cxsdV/lPH1
18+
bFJwVBtyBzbiZ4/kR0ZTBNRX6NVFHokPjiGTVWyWGA5gp8iPSXB3gE9sIBSpw9qL
19+
ArjL+257LQKBgEe4ii9z9/xUZWV4d4eJyRTGIQY63wbD3SXypYfRvDPmO/vLqwoE
20+
rXOk02CrNV1ogGWIWHnQBmxeeMz/7GkmH5W+o7vUd2wziek05/cDJxVWRJJXhiXQ
21+
fdR3vxA7me/iappIXJ28dOVPvDAvjE3hCAnNDj4kJVKHuCdqblPIOIh7AoGAG9kU
22+
hZVrtacXo3770kBWgAjFRxfl9wP3KNt+RfQPRPOvvLnY3cQBH4r7YhmsJAKCGOYx
23+
2RI1yLXQil1VVeI9uGC5+EjSJxvmImUkLENmxniWCeupzuYeFsK+pYTaqaOzYYRA
24+
AhO0nKASCw6LGWoeiZABZffnI2w4sBCPfBfnJG0CgYEAjXqU+Z2KHsajeQcsD5BJ
25+
FWh+XxllgLbNFOVdhiOyicbZVgxhb4rU9JRSGFrRV+5fYVIDaXsPD9FYEx5C9oxx
26+
B5TGPh6p6VF4m2lElKXShOzrvh1QA9Nhpnqmyxtb44EIYaEV34M7W9+juDCycHFX
27+
7bcslckWYTuELLtggVYRV/I=
28+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)