Skip to content

Commit

Permalink
Support proxy jump (#94)
Browse files Browse the repository at this point in the history
This adds support for classical, secure, UDP-based Proxy-Jump.
Let's say we want to connect from A to C through B, B being the proxy. This works with the client located on A connecting to B and establishing UDP forwarding from towards C. By doing so, the client can send regular encrypted QUIC packets through the forwarded socket to connect to C. B cannot manipulate the QUIC packets are they are encrypted and authenticated.

The drawbacks of the approach are:

- quic-go datagram are slow
- we tunnel a congestion-controlled protocol through another congestion-controlled protocol which might cause problems, especially that quic-go is quite prompt to drop UDP datagrams.

So this will work the best for interactive sessions. If troughput is needed, we may want to look at HTTP reverse proxies instead.

Fixes #44

* add -proxy-jump switch

* add proxy-jump integration tests

* parse the UDPProxyJump ssh config option that works like the SSHv2 ProxyJump, without variables (%h, %p, etc are not supported). If used within a classical OpenSSH config file, add `IgnoreUnknown UDPProxyJump` in the config file to avoid OpenSSH errors with this new option.

* bump quic-go version for quic-in-quic capabilities

* bump go version to match what is supported by quic-go

* bump setup-go version for CI build scripts
  • Loading branch information
francoismichel authored Jan 9, 2024
1 parent 9259245 commit 6aa6868
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Set a different ld for each arch # ugly but there are no working flags to explicitly set the ld to use...
run: sudo cp /usr/bin/x86_64-linux-gnu-ld /usr/bin/amd64-ld && sudo cp /tmp/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc /usr/bin/arm64-ld
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: '1.21.x'
- name: Run GoReleaser
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
build-linux:
strategy:
matrix:
go-version: [ '1.20.x', '1.21.x' ]
go-version: [ '1.21.x', '1.22.0-rc.1' ]
goos: [linux]
testuser: [ssh3-testuser]
testpasswd: [ssh3-testpasswd]
Expand All @@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: '${{matrix.go-version}}'
- name: Install dependencies to generate ssh keys and certificates
Expand Down Expand Up @@ -46,7 +46,7 @@ jobs:
- name: log authorized_identities
run: cat ${{matrix.testuserhome}}/.ssh3/authorized_identities
- name: Integration tests
run: sudo -E make -e integration-tests
run: sudo -E PATH=$PATH make -e integration-tests
env:
CERT_PEM: /cert.pem
CERT_PRIV_KEY: /priv.key
Expand All @@ -62,14 +62,14 @@ jobs:
build-macos:
strategy:
matrix:
go-version: [ '1.20.x', '1.21.x' ]
go-version: [ '1.21.x', '1.22.0-rc.1' ]
goos: [darwin]
goarch: [amd64,arm64]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: '${{matrix.go-version}}'
- name: Install dependencies
Expand All @@ -89,7 +89,7 @@ jobs:
build-other-unix:
strategy:
matrix:
go-version: [ '1.20.x', '1.21.x' ]
go-version: [ '1.21.x', '1.22.0-rc.1' ]
goos: [openbsd,freebsd,linux]
goarch: [amd64,"386",arm64,arm]
exclude:
Expand All @@ -101,7 +101,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: '${{matrix.go-version}}'
- name: Install dependencies
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
build-linux:
strategy:
matrix:
go-version: [ '1.20.x', '1.21.x' ]
go-version: [ '1.21.x', '1.22.0-rc.1' ]
goos: [linux]
testuser: [ssh3-testuser]
testpasswd: [ssh3-testpasswd]
Expand All @@ -32,7 +32,7 @@ jobs:
# - name: Install lcrypt for arm64
# run: sudo apt-get -y install libc6:arm64 libcrypt-dev:arm64
- name: Setup Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: '${{matrix.go-version}}'
- name: Lint and vet code
Expand Down
86 changes: 76 additions & 10 deletions cmd/ssh3/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,24 @@ func homedir() string {
// If non-nil, use udpConn as transport (can be used for proxy jump)
// Otherwise, create a UDPConn from udp://host:port
func setupQUICConnection(ctx context.Context, skipHostVerification bool, keylog io.Writer, ssh3Dir string, certPool *x509.CertPool, knownHostsPath string, knownHosts ssh3.KnownHosts,
oidcConfig []*auth.OIDCConfig, options *client.Options, udpConn *net.UDPConn, tty *os.File) (quic.EarlyConnection, int) {
oidcConfig []*auth.OIDCConfig, options *client.Options, proxyRemoteAddr *net.UDPAddr, tty *os.File) (quic.EarlyConnection, int) {

remoteAddr, err := net.ResolveUDPAddr("udp", options.URLHostnamePort())
if err != nil {
log.Error().Msgf("could not resolve UDP address: %s", err)
return nil, -1
}
if udpConn == nil {
udpConn, err = net.ListenUDP("udp", nil)
var err error
remoteAddr := proxyRemoteAddr
if remoteAddr == nil {
remoteAddr, err = net.ResolveUDPAddr("udp", options.URLHostnamePort())
if err != nil {
log.Error().Msgf("could not create UDP connection: %s", err)
log.Error().Msgf("could not resolve UDP address: %s", err)
return nil, -1
}
}

udpConn, err := net.ListenUDP("udp", nil)
if err != nil {
log.Error().Msgf("could not create UDP connection: %s", err)
return nil, -1
}

tlsConf := &tls.Config{
RootCAs: certPool,
InsecureSkipVerify: skipHostVerification,
Expand Down Expand Up @@ -299,6 +302,7 @@ func mainWithStatusCode() int {
forwardSSHAgent := flag.Bool("forward-agent", false, "if set, forwards ssh agent to be used with sshv2 connections on the remote host")
forwardUDP := flag.String("forward-udp", "", "if set, take a localport/remoteip@remoteport forwarding localhost@localport towards remoteip@remoteport")
forwardTCP := flag.String("forward-tcp", "", "if set, take a localport/remoteip@remoteport forwarding localhost@localport towards remoteip@remoteport")
proxyJump := flag.String("proxy-jump", "", "if set, performs a proxy jump using the specified remote host as proxy")
flag.Parse()
args := flag.Args()

Expand Down Expand Up @@ -524,7 +528,69 @@ func mainWithStatusCode() int {
log.Error().Msgf("Could not get connection material for %s: %s", parsedUrl, err)
return -1
}
qconn, status := setupQUICConnection(ctx, *insecure, keyLog, ssh3Dir, pool, knownHostsPath, knownHosts, oidcConfig, options, nil, tty)

if *proxyJump == "" && sshConfig != nil {
*proxyJump, err = sshConfig.Get(parsedUrl.Hostname(), "UDPProxyJump")
if err != nil {
log.Error().Msgf("Could not get UDPProxyJump config value: %s", err)
return -1
}
}

var proxyAddress *net.UDPAddr
if *proxyJump != "" {
if !strings.HasPrefix(*proxyJump, "https://") {
*proxyJump = fmt.Sprintf("https://%s", *proxyJump)
}
proxyParsedUrl, err := url.Parse(*proxyJump)
if err != nil {
log.Error().Msgf("Could not parse proxy host URL %s: %s", *proxyJump, err)
return -1
}
proxyAgentClient, proxyOptions, err := getConnectionMaterialFromURL(proxyParsedUrl, sshConfig, cliAuthMethods)
if err != nil {
log.Error().Msgf("Could not get connection material for proxy %s: %s", proxyParsedUrl, err)
return -1
}
qconn, status := setupQUICConnection(ctx, *insecure, keyLog, ssh3Dir, pool, knownHostsPath, knownHosts, oidcConfig, proxyOptions, nil, tty)

if qconn == nil {
if status != 0 {
log.Error().Msgf("could not setup transport for proxy client: %s", err)
}
return status
}

roundTripper := &http3.RoundTripper{
EnableDatagrams: true,
}

proxyClient, err := client.Dial(ctx, proxyOptions, qconn, roundTripper, proxyAgentClient)
if err != nil {
log.Error().Msgf("could not establish SSH3 proxy conversation: %s", err)
return -1
}

baseAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
if err != nil {
log.Error().Msgf("Could not resolve 127.0.0.1:0: %s", err)
return -1
}
remoteAddr, err := net.ResolveUDPAddr("udp", options.URLHostnamePort())
if err != nil {
log.Error().Msgf("Could not resolve remote address %s: %s", options.URLHostnamePort(), err)
return -1
}
addr, err := proxyClient.ForwardUDP(ctx, baseAddr, remoteAddr)
if err != nil {
log.Error().Msgf("Could not forward UDP for proxy jump: %s", err)
return -1
}
proxyAddress = addr
log.Debug().Msgf("started proxy jump at %s", proxyAddress)
}

qconn, status := setupQUICConnection(ctx, *insecure, keyLog, ssh3Dir, pool, knownHostsPath, knownHosts, oidcConfig, options, proxyAddress, tty)

if qconn == nil {
if status != 0 {
Expand Down
8 changes: 4 additions & 4 deletions conversation.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Conversation struct {
maxPacketSize uint64
defaultDatagramsQueueSize uint64
streamCreator http3.StreamCreator
messageSender util.MessageSender
messageSender util.DatagramSender
channelsManager *channelsManager
context context.Context
cancelContext context.CancelCauseFunc
Expand Down Expand Up @@ -134,7 +134,7 @@ func (c *Conversation) EstablishClientConversation(req *http.Request, roundTripp
// currently does not work for several conversations in the same QUIC connection

for {
dgram, err := qconn.ReceiveMessage(c.Context())
dgram, err := qconn.ReceiveDatagram(c.Context())
if err != nil {
if err != context.Canceled {
log.Error().Msgf("could not receive message from conn: %s", err)
Expand Down Expand Up @@ -166,7 +166,7 @@ func (c *Conversation) EstablishClientConversation(req *http.Request, roundTripp
}
}

func NewServerConversation(ctx context.Context, controlStream http3.Stream, qconn quic.Connection, messageSender util.MessageSender, maxPacketsize uint64) (*Conversation, error) {
func NewServerConversation(ctx context.Context, controlStream http3.Stream, qconn quic.Connection, messageSender util.DatagramSender, maxPacketsize uint64) (*Conversation, error) {
backgroundContext, backgroundCancelFunc := context.WithCancelCause(ctx)

tls := qconn.ConnectionState().TLS
Expand Down Expand Up @@ -290,7 +290,7 @@ func (c *Conversation) getDatagramSenderForChannel(channelID util.ChannelID) fun
buf := util.AppendVarInt(nil, uint64(c.controlStream.StreamID()))
buf = util.AppendVarInt(buf, channelID)
buf = append(buf, datagram...)
return c.messageSender.SendMessage(buf)
return c.messageSender.SendDatagram(buf)
}
}

Expand Down
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/kevinburke/ssh_config v1.2.0
github.com/onsi/ginkgo/v2 v2.13.0
github.com/onsi/gomega v1.29.0
github.com/quic-go/quic-go v0.38.1
github.com/quic-go/quic-go v0.40.1-0.20240102075208-1083d1fb8f98
github.com/rs/zerolog v1.31.0
golang.org/x/crypto v0.14.0
golang.org/x/oauth2 v0.13.0
Expand All @@ -18,15 +18,14 @@ require (
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
go.uber.org/mock v0.3.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.17.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ github.com/quic-go/qtls-go1-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eU
github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.38.1 h1:M36YWA5dEhEeT+slOu/SwMEucbYd0YFidxG3KlGPZaE=
github.com/quic-go/quic-go v0.38.1/go.mod h1:ijnZM7JsFIkp4cRyjxJNIzdSfCLmUMg9wdyhGmg+SN4=
github.com/quic-go/quic-go v0.40.1-0.20240102075208-1083d1fb8f98 h1:XSdekoU+UVlq/Mav+6dYyPuyy5+qzDa/TglBVi+L7Rs=
github.com/quic-go/quic-go v0.40.1-0.20240102075208-1083d1fb8f98/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
Expand All @@ -68,6 +70,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
Expand Down
Loading

0 comments on commit 6aa6868

Please sign in to comment.