mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-07-27 20:09:58 +00:00
TUN-4017: Add support for using cloudflared as a full socks proxy.
To use cloudflared as a socks proxy, add an ingress on the server side with your desired rules. Rules are matched in the order they are added. If there are no rules, it is an implicit allow. If there are rules, but no rule matches match, the connection is denied. ingress: - hostname: socks.example.com service: socks-proxy originRequest: ipRules: - prefix: 1.1.1.1/24 ports: [80, 443] allow: true - prefix: 0.0.0.0/0 allow: false On the client, run using tcp mode: cloudflared access tcp --hostname socks.example.com --url 127.0.0.1:8080 Set your socks proxy as 127.0.0.1:8080 and you will now be proxying all connections to the remote machine.
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/ipaccess"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -26,6 +27,7 @@ var (
|
||||
|
||||
const (
|
||||
ServiceBastion = "bastion"
|
||||
ServiceSocksProxy = "socks-proxy"
|
||||
ServiceWarpRouting = "warp-routing"
|
||||
)
|
||||
|
||||
@@ -175,6 +177,23 @@ func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestCon
|
||||
service = &srv
|
||||
} else if r.Service == "hello_world" || r.Service == "hello-world" || r.Service == "helloworld" {
|
||||
service = new(helloWorld)
|
||||
} else if r.Service == ServiceSocksProxy {
|
||||
rules := make([]ipaccess.Rule, len(r.OriginRequest.IPRules))
|
||||
|
||||
for i, ipRule := range r.OriginRequest.IPRules {
|
||||
rule, err := ipaccess.NewRuleByCIDR(ipRule.Prefix, ipRule.Ports, ipRule.Allow)
|
||||
if err != nil {
|
||||
return Ingress{}, fmt.Errorf("unable to create ip rule for %s: %s", r.Service, err)
|
||||
}
|
||||
rules[i] = rule
|
||||
}
|
||||
|
||||
accessPolicy, err := ipaccess.NewPolicy(false, rules)
|
||||
if err != nil {
|
||||
return Ingress{}, fmt.Errorf("unable to create ip access policy for %s: %s", r.Service, err)
|
||||
}
|
||||
|
||||
service = newSocksProxyOverWSService(accessPolicy)
|
||||
} else if r.Service == ServiceBastion || cfg.BastionMode {
|
||||
// Bastion mode will always start a Websocket proxy server, which will
|
||||
// overwrite the localService.URL field when `start` is called. So,
|
||||
|
@@ -15,6 +15,7 @@ import (
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/ipaccess"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
)
|
||||
|
||||
@@ -304,6 +305,33 @@ ingress:
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SOCKS services",
|
||||
args: args{rawYAML: `
|
||||
ingress:
|
||||
- hostname: socks.foo.com
|
||||
service: socks-proxy
|
||||
originRequest:
|
||||
ipRules:
|
||||
- prefix: 1.1.1.0/24
|
||||
ports: [80, 443]
|
||||
allow: true
|
||||
- prefix: 0.0.0.0/0
|
||||
allow: false
|
||||
- service: http_status:404
|
||||
`},
|
||||
want: []Rule{
|
||||
{
|
||||
Hostname: "socks.foo.com",
|
||||
Service: newSocksProxyOverWSService(accessPolicy()),
|
||||
Config: defaultConfig,
|
||||
},
|
||||
{
|
||||
Service: &fourOhFour,
|
||||
Config: defaultConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URL isn't necessary if using bastion",
|
||||
args: args{rawYAML: `
|
||||
@@ -548,6 +576,16 @@ func MustParseURL(t *testing.T, rawURL string) *url.URL {
|
||||
return u
|
||||
}
|
||||
|
||||
func accessPolicy() *ipaccess.Policy {
|
||||
cidr1 := "1.1.1.0/24"
|
||||
cidr2 := "0.0.0.0/0"
|
||||
rule1, _ := ipaccess.NewRuleByCIDR(&cidr1, []int{80, 443}, true)
|
||||
rule2, _ := ipaccess.NewRuleByCIDR(&cidr2, nil, false)
|
||||
rules := []ipaccess.Rule{rule1, rule2}
|
||||
accessPolicy, _ := ipaccess.NewPolicy(false, rules)
|
||||
return accessPolicy
|
||||
}
|
||||
|
||||
func BenchmarkFindMatch(b *testing.B) {
|
||||
rulesYAML := `
|
||||
ingress:
|
||||
|
@@ -7,6 +7,8 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/cloudflare/cloudflared/ipaccess"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
gws "github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -107,3 +109,17 @@ func newWSConnection(clientTLSConfig *tls.Config, r *http.Request) (OriginConnec
|
||||
resp,
|
||||
}, resp, nil
|
||||
}
|
||||
|
||||
// socksProxyOverWSConnection is an OriginConnection that streams SOCKS connections over WS.
|
||||
// The connection to the origin happens inside the SOCKS code as the client specifies the origin
|
||||
// details in the packet.
|
||||
type socksProxyOverWSConnection struct {
|
||||
accessPolicy *ipaccess.Policy
|
||||
}
|
||||
|
||||
func (sp *socksProxyOverWSConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
|
||||
socks.StreamNetHandler(websocket.NewConn(ctx, tunnelConn, log), sp.accessPolicy, log)
|
||||
}
|
||||
|
||||
func (sp *socksProxyOverWSConnection) Close() {
|
||||
}
|
||||
|
@@ -145,3 +145,14 @@ func (o *tcpOverWSService) bastionDest(r *http.Request) (string, error) {
|
||||
func removePath(dest string) string {
|
||||
return strings.SplitN(dest, "/", 2)[0]
|
||||
}
|
||||
|
||||
func (o *socksProxyOverWSService) EstablishConnection(r *http.Request) (OriginConnection, *http.Response, error) {
|
||||
originConn := o.conn
|
||||
resp := &http.Response{
|
||||
Status: switchingProtocolText,
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
Header: websocket.NewResponseHeader(r),
|
||||
ContentLength: -1,
|
||||
}
|
||||
return originConn, resp, nil
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package ingress
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/ipaccess"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
@@ -213,6 +214,8 @@ type OriginRequestConfig struct {
|
||||
ProxyPort uint `yaml:"proxyPort"`
|
||||
// What sort of proxy should be started
|
||||
ProxyType string `yaml:"proxyType"`
|
||||
// IP rules for the proxy service
|
||||
IPRules []ipaccess.Rule `yaml:"ipRules"`
|
||||
}
|
||||
|
||||
func (defaults *OriginRequestConfig) setConnectTimeout(overrides config.OriginRequestConfig) {
|
||||
|
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/ipaccess"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
@@ -100,6 +101,10 @@ type tcpOverWSService struct {
|
||||
streamHandler streamHandlerFunc
|
||||
}
|
||||
|
||||
type socksProxyOverWSService struct {
|
||||
conn *socksProxyOverWSConnection
|
||||
}
|
||||
|
||||
func newTCPOverWSService(url *url.URL) *tcpOverWSService {
|
||||
switch url.Scheme {
|
||||
case "ssh":
|
||||
@@ -122,6 +127,16 @@ func newBastionService() *tcpOverWSService {
|
||||
}
|
||||
}
|
||||
|
||||
func newSocksProxyOverWSService(accessPolicy *ipaccess.Policy) *socksProxyOverWSService {
|
||||
proxy := socksProxyOverWSService{
|
||||
conn: &socksProxyOverWSConnection{
|
||||
accessPolicy: accessPolicy,
|
||||
},
|
||||
}
|
||||
|
||||
return &proxy
|
||||
}
|
||||
|
||||
func addPortIfMissing(uri *url.URL, port int) {
|
||||
if uri.Port() == "" {
|
||||
uri.Host = fmt.Sprintf("%s:%d", uri.Hostname(), port)
|
||||
@@ -144,6 +159,14 @@ func (o *tcpOverWSService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdo
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *socksProxyOverWSService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *socksProxyOverWSService) String() string {
|
||||
return ServiceSocksProxy
|
||||
}
|
||||
|
||||
// HelloWorld is an OriginService for the built-in Hello World server.
|
||||
// Users only use this for testing and experimenting with cloudflared.
|
||||
type helloWorld struct {
|
||||
|
Reference in New Issue
Block a user