diff --git a/README.md b/README.md index 2b4d2fc5..961fd38b 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,10 @@ $ chisel client --help which does reverse port forwarding, sharing : from the client to the server's :. + additionally, P can be appended after the R flag to send a PROXY v2 header + which can be used to identify the original source of incoming connections: + + RP::::/ example remotes @@ -247,6 +251,7 @@ $ chisel client --help socks 5000:socks R:2222:localhost:22 + RP:2222:localhost:22 R:socks R:5000:socks stdio:example.com:22 @@ -264,7 +269,9 @@ $ chisel client --help will be proxied through the client which specified the remote. Reverse remotes specifying "R:socks" will listen on the server's default socks port (1080) and terminate the connection at the - client's internal SOCKS5 proxy. + client's internal SOCKS5 proxy. Additionally, P can be appended after + the R prefix to have the chisel server pass a PROXY v2 header + which can be used to identify the original source of incoming connections. When stdio is used as local-host, the tunnel will connect standard input/output of this program with the remote. This is useful when diff --git a/go.mod b/go.mod index 34f0abe2..4fe27da1 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,9 @@ require ( github.com/jpillora/backoff v1.0.0 github.com/jpillora/requestlog v1.0.0 github.com/jpillora/sizestr v1.0.0 - golang.org/x/crypto v0.16.0 - golang.org/x/net v0.14.0 + github.com/pires/go-proxyproto v0.8.0 + golang.org/x/crypto v0.21.0 + golang.org/x/net v0.23.0 golang.org/x/sync v0.5.0 ) @@ -18,6 +19,6 @@ require ( github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect github.com/jpillora/ansi v1.0.3 // indirect github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index e2f4898d..01925b20 100644 --- a/go.sum +++ b/go.sum @@ -14,18 +14,20 @@ github.com/jpillora/requestlog v1.0.0 h1:bg++eJ74T7DYL3DlIpiwknrtfdUA9oP/M4fL+Pp github.com/jpillora/requestlog v1.0.0/go.mod h1:HTWQb7QfDc2jtHnWe2XEIEeJB7gJPnVdpNn52HXPvy8= github.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2ygw= github.com/jpillora/sizestr v1.0.0/go.mod h1:bUhLv4ctkknatr6gR42qPxirmd5+ds1u7mzD+MZ33f0= +github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0= +github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/main.go b/main.go index 01f9ca3b..6f0f8cf5 100644 --- a/main.go +++ b/main.go @@ -323,6 +323,10 @@ var clientHelp = ` which does reverse port forwarding, sharing : from the client to the server's :. + additionally, P can be appended after the R flag to send a PROXY v2 header + which can be used to identify the original source of incoming connections: + + RP::::/ example remotes @@ -333,6 +337,7 @@ var clientHelp = ` socks 5000:socks R:2222:localhost:22 + RP:2222:localhost:22 R:socks R:5000:socks stdio:example.com:22 @@ -350,7 +355,9 @@ var clientHelp = ` will be proxied through the client which specified the remote. Reverse remotes specifying "R:socks" will listen on the server's default socks port (1080) and terminate the connection at the - client's internal SOCKS5 proxy. + client's internal SOCKS5 proxy. Additionally, P can be appended after + the R prefix to have the chisel server pass a PROXY v2 header + which can be used to identify the original source of incoming connections. When stdio is used as local-host, the tunnel will connect standard input/output of this program with the remote. This is useful when diff --git a/share/settings/remote.go b/share/settings/remote.go index bfe6cbae..1f4becb2 100644 --- a/share/settings/remote.go +++ b/share/settings/remote.go @@ -35,14 +35,20 @@ import ( type Remote struct { LocalHost, LocalPort, LocalProto string RemoteHost, RemotePort, RemoteProto string - Socks, Reverse, Stdio bool + Socks, Reverse, Stdio, ProxyProto bool } const revPrefix = "R:" +const revProxyPrefix = "RP:" func DecodeRemote(s string) (*Remote, error) { + proxy := false reverse := false - if strings.HasPrefix(s, revPrefix) { + if strings.HasPrefix(s, revProxyPrefix) { + s = strings.TrimPrefix(s, revProxyPrefix) + reverse = true + proxy = true + } else if strings.HasPrefix(s, revPrefix) { s = strings.TrimPrefix(s, revPrefix) reverse = true } @@ -50,7 +56,7 @@ func DecodeRemote(s string) (*Remote, error) { if len(parts) <= 0 || len(parts) >= 5 { return nil, errors.New("Invalid remote") } - r := &Remote{Reverse: reverse} + r := &Remote{Reverse: reverse, ProxyProto: proxy} //parse from back to front, to set 'remote' fields first, //then to set 'local' fields second (allows the 'remote' side //to provide the defaults) @@ -129,6 +135,9 @@ func DecodeRemote(s string) (*Remote, error) { if r.Stdio && r.Reverse { return nil, errors.New("stdio cannot be reversed") } + if r.ProxyProto && !r.Reverse { + return nil, errors.New("cannot use proxy protocol for a non-reversed remote") + } return r, nil } @@ -165,7 +174,9 @@ func L4Proto(s string) (head, proto string) { //implement Stringer func (r Remote) String() string { sb := strings.Builder{} - if r.Reverse { + if r.Reverse && r.ProxyProto { + sb.WriteString(revProxyPrefix) + } else if r.Reverse { sb.WriteString(revPrefix) } sb.WriteString(strings.TrimPrefix(r.Local(), "0.0.0.0:")) @@ -187,7 +198,9 @@ func (r Remote) Encode() string { if r.RemoteProto == "udp" { remote += "/udp" } - if r.Reverse { + if r.Reverse && r.ProxyProto { + return "RP:" + local + ":" + remote + } else if r.Reverse { return "R:" + local + ":" + remote } return local + ":" + remote diff --git a/share/settings/remote_test.go b/share/settings/remote_test.go index b2ba39bb..9cb5f9d4 100644 --- a/share/settings/remote_test.go +++ b/share/settings/remote_test.go @@ -40,6 +40,17 @@ func TestRemoteDecode(t *testing.T) { }, "R:0.0.0.0:80:google.com:80", }, + { + "RP:google.com:80", + Remote{ + LocalPort: "80", + RemoteHost: "google.com", + RemotePort: "80", + Reverse: true, + ProxyProto: true, + }, + "RP:0.0.0.0:80:google.com:80", + }, { "示例網站.com:80", Remote{ @@ -111,6 +122,18 @@ func TestRemoteDecode(t *testing.T) { }, "R:[::]:3000:[::1]:3000", }, + { + "RP:[::]:3000:[::1]:3000", + Remote{ + LocalHost: "[::]", + LocalPort: "3000", + RemoteHost: "[::1]", + RemotePort: "3000", + Reverse: true, + ProxyProto: true, + }, + "RP:[::]:3000:[::1]:3000", + }, } { //expected defaults expected := test.Output diff --git a/share/tunnel/tunnel_in_proxy.go b/share/tunnel/tunnel_in_proxy.go index 007fb0c7..4712cb7a 100644 --- a/share/tunnel/tunnel_in_proxy.go +++ b/share/tunnel/tunnel_in_proxy.go @@ -9,6 +9,7 @@ import ( "github.com/jpillora/chisel/share/cio" "github.com/jpillora/chisel/share/settings" "github.com/jpillora/sizestr" + "github.com/pires/go-proxyproto" "golang.org/x/crypto/ssh" ) @@ -144,6 +145,22 @@ func (p *Proxy) pipeRemote(ctx context.Context, src io.ReadWriteCloser) { return } go ssh.DiscardRequests(reqs) + //if proxy protocol is requested, send the header + if p.remote.ProxyProto { + conn, ok := src.(net.Conn) + if !ok { + //this should never happen, and if it does, something has gone horribly wrong (file an issue) + panic("attempted to use proxy protocol for a source which is not a network connection") + } + + header := proxyproto.HeaderProxyFromAddrs(2, conn.RemoteAddr(), conn.LocalAddr()) + s, err := header.WriteTo(dst) + if err != nil { + l.Infof("Stream error: %s", err) + return + } + l.Debugf("PROXY v2 header (sent %s)", sizestr.ToString(s)) + } //then pipe s, r := cio.Pipe(src, dst) l.Debugf("Close (sent %s received %s)", sizestr.ToString(s), sizestr.ToString(r)) diff --git a/share/tunnel/tunnel_in_proxy_udp.go b/share/tunnel/tunnel_in_proxy_udp.go index 3f3fa8be..7eac261d 100644 --- a/share/tunnel/tunnel_in_proxy_udp.go +++ b/share/tunnel/tunnel_in_proxy_udp.go @@ -4,6 +4,7 @@ import ( "context" "encoding/gob" "fmt" + "github.com/pires/go-proxyproto" "io" "net" "strings" @@ -102,8 +103,20 @@ func (u *udpListener) runInbound(ctx context.Context) error { } return u.Errorf("inbound-udpchan: %w", err) } - //send over channel, including source address b := buff[:n] + //if proxy protocol is requested, prepend the header + if u.remote.ProxyProto { + //NOTE: LocalAddr for UDP doesn't actually get the destination IP in the packet + //getting that information is non-trivial and non-portable from what I can see + //therefore, this will suffice for now + header := proxyproto.HeaderProxyFromAddrs(2, addr, u.inbound.LocalAddr()) + formatted, err := header.Format() + if err != nil { + return u.Errorf("header format: %w", err) + } + b = append(formatted, b...) + } + //send over channel, including source address if err := uc.encode(addr.String(), b); err != nil { if strings.HasSuffix(err.Error(), "EOF") { continue //dropped packet... diff --git a/test/e2e/base_test.go b/test/e2e/base_test.go index bb344cbf..a1e800a8 100644 --- a/test/e2e/base_test.go +++ b/test/e2e/base_test.go @@ -46,3 +46,24 @@ func TestReverse(t *testing.T) { t.Fatalf("expected exclamation mark added") } } + +func TestReverseProxyProtocol(t *testing.T) { + tmpPort := availablePort() + //setup server, client, fileserver + teardown := simpleSetup(t, + &chserver.Config{ + Reverse: true, + }, + &chclient.Config{ + Remotes: []string{"RP:" + tmpPort + ":$FILEPORT"}, + }) + defer teardown() + //test remote (this goes through the server and out the client) + result, err := post("http://localhost:"+tmpPort, "foo") + if err != nil { + t.Fatal(err) + } + if result != "foo!" { + t.Fatalf("expected exclamation mark added") + } +} diff --git a/test/e2e/setup_test.go b/test/e2e/setup_test.go index c1611bfb..90d4c5e6 100644 --- a/test/e2e/setup_test.go +++ b/test/e2e/setup_test.go @@ -2,6 +2,7 @@ package e2e_test import ( "context" + "github.com/pires/go-proxyproto" "io" "log" "net" @@ -44,14 +45,18 @@ func (tl *testLayout) setup(t *testing.T) (server *chserver.Server, client *chcl if err != nil { t.Fatal(err) } + pl := &proxyproto.Listener{ + Listener: fl, + } log.Printf("fileserver: listening on %s", fileAddr) go func() { - f.Serve(fl) + f.Serve(pl) cancel() }() go func() { <-ctx.Done() f.Close() + pl.Close() }() } //server