From 31bc4b683309b345a3e6e9c02cd247ff017cdc31 Mon Sep 17 00:00:00 2001 From: Michael Quigley Date: Wed, 7 Feb 2024 13:59:34 -0500 Subject: [PATCH 1/9] initial socks backend (#558) --- endpoints/socks/backend.go | 53 +++++ endpoints/socks/socks5.go | 403 +++++++++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 endpoints/socks/backend.go create mode 100755 endpoints/socks/socks5.go diff --git a/endpoints/socks/backend.go b/endpoints/socks/backend.go new file mode 100644 index 00000000..1ad152b4 --- /dev/null +++ b/endpoints/socks/backend.go @@ -0,0 +1,53 @@ +package socks + +import ( + "github.com/openziti/sdk-golang/ziti" + "github.com/openziti/sdk-golang/ziti/edge" + "github.com/openziti/zrok/endpoints" + "github.com/pkg/errors" + "time" +) + +type BackendConfig struct { + IdentityPath string + ShrToken string + Requests chan *endpoints.Request +} + +type Backend struct { + cfg *BackendConfig + listener edge.Listener + server *Server +} + +func NewBackend(cfg *BackendConfig) (*Backend, error) { + options := ziti.ListenOptions{ + ConnectTimeout: 5 * time.Minute, + WaitForNEstablishedListeners: 1, + } + zcfg, err := ziti.NewConfigFromFile(cfg.IdentityPath) + if err != nil { + return nil, errors.Wrap(err, "error loading ziti identity") + } + zctx, err := ziti.NewContext(zcfg) + if err != nil { + return nil, errors.Wrap(err, "error loading ziti context") + } + listener, err := zctx.ListenWithOptions(cfg.ShrToken, &options) + if err != nil { + return nil, err + } + + return &Backend{ + cfg: cfg, + listener: listener, + server: &Server{}, + }, nil +} + +func (b *Backend) Run() error { + if err := b.server.Serve(b.listener); err != nil { + return err + } + return nil +} diff --git a/endpoints/socks/socks5.go b/endpoints/socks/socks5.go new file mode 100755 index 00000000..8b1d1593 --- /dev/null +++ b/endpoints/socks/socks5.go @@ -0,0 +1,403 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Package socks5 is a SOCKS5 server implementation. +// +// This is used for userspace networking in Tailscale. Specifically, +// this is used for dialing out of the machine to other nodes, without +// the host kernel's involvement, so it doesn't proper routing tables, +// TUN, IPv6, etc. This package is meant to only handle the SOCKS5 protocol +// details and not any integration with Tailscale internals itself. +// +// The glue between this package and Tailscale is in net/socks5/tssocks. +package socks + +import ( + "context" + "encoding/binary" + "fmt" + "github.com/sirupsen/logrus" + "io" + "net" + "strconv" + "time" +) + +// Authentication METHODs described in RFC 1928, section 3. +const ( + noAuthRequired byte = 0 + passwordAuth byte = 2 + noAcceptableAuth byte = 255 +) + +// passwordAuthVersion is the auth version byte described in RFC 1929. +const passwordAuthVersion = 1 + +// socks5Version is the byte that represents the SOCKS version +// in requests. +const socks5Version byte = 5 + +// commandType are the bytes sent in SOCKS5 packets +// that represent the kind of connection the client needs. +type commandType byte + +// The set of valid SOCKS5 commands as described in RFC 1928. +const ( + connect commandType = 1 + bind commandType = 2 + udpAssociate commandType = 3 +) + +// addrType are the bytes sent in SOCKS5 packets +// that represent particular address types. +type addrType byte + +// The set of valid SOCKS5 address types as defined in RFC 1928. +const ( + ipv4 addrType = 1 + domainName addrType = 3 + ipv6 addrType = 4 +) + +// replyCode are the bytes sent in SOCKS5 packets +// that represent replies from the server to a client +// request. +type replyCode byte + +// The set of valid SOCKS5 reply types as per the RFC 1928. +const ( + success replyCode = 0 + generalFailure replyCode = 1 + connectionNotAllowed replyCode = 2 + networkUnreachable replyCode = 3 + hostUnreachable replyCode = 4 + connectionRefused replyCode = 5 + ttlExpired replyCode = 6 + commandNotSupported replyCode = 7 + addrTypeNotSupported replyCode = 8 +) + +// Server is a SOCKS5 proxy server. +type Server struct { + // Dialer optionally specifies the dialer to use for outgoing connections. + // If nil, the net package's standard dialer is used. + Dialer func(ctx context.Context, network, addr string) (net.Conn, error) + + // Username and Password, if set, are the credential clients must provide. + Username string + Password string +} + +func (s *Server) dial(ctx context.Context, network, addr string) (net.Conn, error) { + dial := s.Dialer + if dial == nil { + dialer := &net.Dialer{} + dial = dialer.DialContext + } + return dial(ctx, network, addr) +} + +// Serve accepts and handles incoming connections on the given listener. +func (s *Server) Serve(l net.Listener) error { + defer l.Close() + for { + c, err := l.Accept() + if err != nil { + return err + } + go func() { + defer c.Close() + conn := &Conn{clientConn: c, srv: s} + err := conn.Run() + if err != nil { + logrus.Infof("client connection failed: %v", err) + } + }() + } +} + +// Conn is a SOCKS5 connection for client to reach +// server. +type Conn struct { + // The struct is filled by each of the internal + // methods in turn as the transaction progresses. + + srv *Server + clientConn net.Conn + request *request +} + +// Run starts the new connection. +func (c *Conn) Run() error { + needAuth := c.srv.Username != "" || c.srv.Password != "" + authMethod := noAuthRequired + if needAuth { + authMethod = passwordAuth + } + + err := parseClientGreeting(c.clientConn, authMethod) + if err != nil { + c.clientConn.Write([]byte{socks5Version, noAcceptableAuth}) + return err + } + c.clientConn.Write([]byte{socks5Version, authMethod}) + if !needAuth { + return c.handleRequest() + } + + user, pwd, err := parseClientAuth(c.clientConn) + if err != nil || user != c.srv.Username || pwd != c.srv.Password { + c.clientConn.Write([]byte{1, 1}) // auth error + return err + } + c.clientConn.Write([]byte{1, 0}) // auth success + + return c.handleRequest() +} + +func (c *Conn) handleRequest() error { + req, err := parseClientRequest(c.clientConn) + if err != nil { + res := &response{reply: generalFailure} + buf, _ := res.marshal() + c.clientConn.Write(buf) + return err + } + if req.command != connect { + res := &response{reply: commandNotSupported} + buf, _ := res.marshal() + c.clientConn.Write(buf) + return fmt.Errorf("unsupported command %v", req.command) + } + c.request = req + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + srv, err := c.srv.dial( + ctx, + "tcp", + net.JoinHostPort(c.request.destination, strconv.Itoa(int(c.request.port))), + ) + if err != nil { + res := &response{reply: generalFailure} + buf, _ := res.marshal() + c.clientConn.Write(buf) + return err + } + defer srv.Close() + serverAddr, serverPortStr, err := net.SplitHostPort(srv.LocalAddr().String()) + if err != nil { + return err + } + serverPort, _ := strconv.Atoi(serverPortStr) + + var bindAddrType addrType + if ip := net.ParseIP(serverAddr); ip != nil { + if ip.To4() != nil { + bindAddrType = ipv4 + } else { + bindAddrType = ipv6 + } + } else { + bindAddrType = domainName + } + res := &response{ + reply: success, + bindAddrType: bindAddrType, + bindAddr: serverAddr, + bindPort: uint16(serverPort), + } + buf, err := res.marshal() + if err != nil { + res = &response{reply: generalFailure} + buf, _ = res.marshal() + } + c.clientConn.Write(buf) + + errc := make(chan error, 2) + go func() { + _, err := io.Copy(c.clientConn, srv) + if err != nil { + err = fmt.Errorf("from backend to client: %w", err) + } + errc <- err + }() + go func() { + _, err := io.Copy(srv, c.clientConn) + if err != nil { + err = fmt.Errorf("from client to backend: %w", err) + } + errc <- err + }() + return <-errc +} + +// parseClientGreeting parses a request initiation packet. +func parseClientGreeting(r io.Reader, authMethod byte) error { + var hdr [2]byte + _, err := io.ReadFull(r, hdr[:]) + if err != nil { + return fmt.Errorf("could not read packet header") + } + if hdr[0] != socks5Version { + return fmt.Errorf("incompatible SOCKS version") + } + count := int(hdr[1]) + methods := make([]byte, count) + _, err = io.ReadFull(r, methods) + if err != nil { + return fmt.Errorf("could not read methods") + } + for _, m := range methods { + if m == authMethod { + return nil + } + } + return fmt.Errorf("no acceptable auth methods") +} + +func parseClientAuth(r io.Reader) (usr, pwd string, err error) { + var hdr [2]byte + if _, err := io.ReadFull(r, hdr[:]); err != nil { + return "", "", fmt.Errorf("could not read auth packet header") + } + if hdr[0] != passwordAuthVersion { + return "", "", fmt.Errorf("bad SOCKS auth version") + } + usrLen := int(hdr[1]) + usrBytes := make([]byte, usrLen) + if _, err := io.ReadFull(r, usrBytes); err != nil { + return "", "", fmt.Errorf("could not read auth packet username") + } + var hdrPwd [1]byte + if _, err := io.ReadFull(r, hdrPwd[:]); err != nil { + return "", "", fmt.Errorf("could not read auth packet password length") + } + pwdLen := int(hdrPwd[0]) + pwdBytes := make([]byte, pwdLen) + if _, err := io.ReadFull(r, pwdBytes); err != nil { + return "", "", fmt.Errorf("could not read auth packet password") + } + return string(usrBytes), string(pwdBytes), nil +} + +// request represents data contained within a SOCKS5 +// connection request packet. +type request struct { + command commandType + destination string + port uint16 + destAddrType addrType +} + +// parseClientRequest converts raw packet bytes into a +// SOCKS5Request struct. +func parseClientRequest(r io.Reader) (*request, error) { + var hdr [4]byte + _, err := io.ReadFull(r, hdr[:]) + if err != nil { + return nil, fmt.Errorf("could not read packet header") + } + cmd := hdr[1] + destAddrType := addrType(hdr[3]) + + var destination string + var port uint16 + + if destAddrType == ipv4 { + var ip [4]byte + _, err = io.ReadFull(r, ip[:]) + if err != nil { + return nil, fmt.Errorf("could not read IPv4 address") + } + destination = net.IP(ip[:]).String() + } else if destAddrType == domainName { + var dstSizeByte [1]byte + _, err = io.ReadFull(r, dstSizeByte[:]) + if err != nil { + return nil, fmt.Errorf("could not read domain name size") + } + dstSize := int(dstSizeByte[0]) + domainName := make([]byte, dstSize) + _, err = io.ReadFull(r, domainName) + if err != nil { + return nil, fmt.Errorf("could not read domain name") + } + destination = string(domainName) + } else if destAddrType == ipv6 { + var ip [16]byte + _, err = io.ReadFull(r, ip[:]) + if err != nil { + return nil, fmt.Errorf("could not read IPv6 address") + } + destination = net.IP(ip[:]).String() + } else { + return nil, fmt.Errorf("unsupported address type") + } + var portBytes [2]byte + _, err = io.ReadFull(r, portBytes[:]) + if err != nil { + return nil, fmt.Errorf("could not read port") + } + port = binary.BigEndian.Uint16(portBytes[:]) + + return &request{ + command: commandType(cmd), + destination: destination, + port: port, + destAddrType: destAddrType, + }, nil +} + +// response contains the contents of +// a response packet sent from the proxy +// to the client. +type response struct { + reply replyCode + bindAddrType addrType + bindAddr string + bindPort uint16 +} + +// marshal converts a SOCKS5Response struct into +// a packet. If res.reply == Success, it may throw an error on +// receiving an invalid bind address. Otherwise, it will not throw. +func (res *response) marshal() ([]byte, error) { + pkt := make([]byte, 4) + pkt[0] = socks5Version + pkt[1] = byte(res.reply) + pkt[2] = 0 // null reserved byte + pkt[3] = byte(res.bindAddrType) + + if res.reply != success { + return pkt, nil + } + + var addr []byte + switch res.bindAddrType { + case ipv4: + addr = net.ParseIP(res.bindAddr).To4() + if addr == nil { + return nil, fmt.Errorf("invalid IPv4 address for binding") + } + case domainName: + if len(res.bindAddr) > 255 { + return nil, fmt.Errorf("invalid domain name for binding") + } + addr = make([]byte, 0, len(res.bindAddr)+1) + addr = append(addr, byte(len(res.bindAddr))) + addr = append(addr, []byte(res.bindAddr)...) + case ipv6: + addr = net.ParseIP(res.bindAddr).To16() + if addr == nil { + return nil, fmt.Errorf("invalid IPv6 address for binding") + } + default: + return nil, fmt.Errorf("unsupported address type") + } + + pkt = append(pkt, addr...) + pkt = binary.BigEndian.AppendUint16(pkt, uint16(res.bindPort)) + + return pkt, nil +} From 1db29fdc11be88ff6925a9b2bc487abb6dffda69 Mon Sep 17 00:00:00 2001 From: Michael Quigley Date: Thu, 8 Feb 2024 13:11:19 -0500 Subject: [PATCH 2/9] store and api changes for socks backend (#558) --- .../017_v0_4_24_backend_mode_socks.sql | 3 + .../017_v0_4_24_backend_mode_socks.sql | 57 +++++++++++++++++++ rest_model_zrok/share_request.go | 7 ++- rest_server_zrok/embedded_spec.go | 6 +- .../sdk/zrok/zrok_api/models/share_request.py | 2 +- specs/zrok.yml | 2 +- 6 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 controller/store/sql/postgresql/017_v0_4_24_backend_mode_socks.sql create mode 100644 controller/store/sql/sqlite3/017_v0_4_24_backend_mode_socks.sql diff --git a/controller/store/sql/postgresql/017_v0_4_24_backend_mode_socks.sql b/controller/store/sql/postgresql/017_v0_4_24_backend_mode_socks.sql new file mode 100644 index 00000000..501dcf11 --- /dev/null +++ b/controller/store/sql/postgresql/017_v0_4_24_backend_mode_socks.sql @@ -0,0 +1,3 @@ +-- +migrate Up + +alter type backend_mode add value 'socks'; \ No newline at end of file diff --git a/controller/store/sql/sqlite3/017_v0_4_24_backend_mode_socks.sql b/controller/store/sql/sqlite3/017_v0_4_24_backend_mode_socks.sql new file mode 100644 index 00000000..cfa65fab --- /dev/null +++ b/controller/store/sql/sqlite3/017_v0_4_24_backend_mode_socks.sql @@ -0,0 +1,57 @@ +-- +migrate Up + +alter table shares rename to shares_old; +create table shares ( + id integer primary key, + environment_id integer constraint fk_environments_shares references environments on delete cascade, + z_id string not null unique, + token string not null, + share_mode string not null, + backend_mode string not null, + frontend_selection string, + frontend_endpoint string, + backend_proxy_endpoint string, + reserved boolean not null default(false), + created_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')), + updated_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')), + deleted boolean not null default(false), + + constraint chk_z_id check (z_id <> ''), + constraint chk_token check (token <> ''), + constraint chk_share_mode check (share_mode == 'public' or share_mode == 'private'), + constraint chk_backend_mode check (backend_mode == 'proxy' or backend_mode == 'web' or backend_mode == 'tcpTunnel' or backend_mode == 'udpTunnel' or backend_mode == 'caddy' or backend_mode == 'drive' or backend_mode == 'socks') +); +insert into shares select * from shares_old; +create unique index shares_token_idx ON shares(token) WHERE deleted is false; + +alter table frontends rename to frontends_old; +create table frontends ( + id integer primary key, + environment_id integer references environments(id), + token varchar(32) not null unique, + z_id varchar(32) not null, + public_name varchar(64) unique, + url_template varchar(1024), + reserved boolean not null default(false), + created_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')), + updated_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')), + deleted boolean not null default(false), + private_share_id integer references shares(id) +); +insert into frontends select * from frontends_old; +drop table frontends_old; + +alter table share_limit_journal rename to share_limit_journal_old; +create table share_limit_journal ( + id integer primary key, + share_id integer references shares(id), + rx_bytes bigint not null, + tx_bytes bigint not null, + action limit_action_type not null, + created_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')), + updated_at datetime not null default(strftime('%Y-%m-%d %H:%M:%f', 'now')) +); +insert into share_limit_journal select * from share_limit_journal_old; +drop table share_limit_journal_old; + +drop table shares_old; \ No newline at end of file diff --git a/rest_model_zrok/share_request.go b/rest_model_zrok/share_request.go index ae118a96..6125bd95 100644 --- a/rest_model_zrok/share_request.go +++ b/rest_model_zrok/share_request.go @@ -28,7 +28,7 @@ type ShareRequest struct { AuthUsers []*AuthUser `json:"authUsers"` // backend mode - // Enum: [proxy web tcpTunnel udpTunnel caddy drive] + // Enum: [proxy web tcpTunnel udpTunnel caddy drive socks] BackendMode string `json:"backendMode,omitempty"` // backend proxy endpoint @@ -117,7 +117,7 @@ var shareRequestTypeBackendModePropEnum []interface{} func init() { var res []string - if err := json.Unmarshal([]byte(`["proxy","web","tcpTunnel","udpTunnel","caddy","drive"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["proxy","web","tcpTunnel","udpTunnel","caddy","drive","socks"]`), &res); err != nil { panic(err) } for _, v := range res { @@ -144,6 +144,9 @@ const ( // ShareRequestBackendModeDrive captures enum value "drive" ShareRequestBackendModeDrive string = "drive" + + // ShareRequestBackendModeSocks captures enum value "socks" + ShareRequestBackendModeSocks string = "socks" ) // prop value enum diff --git a/rest_server_zrok/embedded_spec.go b/rest_server_zrok/embedded_spec.go index 08c43fd2..fe803bd9 100644 --- a/rest_server_zrok/embedded_spec.go +++ b/rest_server_zrok/embedded_spec.go @@ -1476,7 +1476,8 @@ func init() { "tcpTunnel", "udpTunnel", "caddy", - "drive" + "drive", + "socks" ] }, "backendProxyEndpoint": { @@ -3099,7 +3100,8 @@ func init() { "tcpTunnel", "udpTunnel", "caddy", - "drive" + "drive", + "socks" ] }, "backendProxyEndpoint": { diff --git a/sdk/python/sdk/zrok/zrok_api/models/share_request.py b/sdk/python/sdk/zrok/zrok_api/models/share_request.py index 61616449..331fa41a 100644 --- a/sdk/python/sdk/zrok/zrok_api/models/share_request.py +++ b/sdk/python/sdk/zrok/zrok_api/models/share_request.py @@ -184,7 +184,7 @@ class ShareRequest(object): :param backend_mode: The backend_mode of this ShareRequest. # noqa: E501 :type: str """ - allowed_values = ["proxy", "web", "tcpTunnel", "udpTunnel", "caddy", "drive"] # noqa: E501 + allowed_values = ["proxy", "web", "tcpTunnel", "udpTunnel", "caddy", "drive", "socks"] # noqa: E501 if backend_mode not in allowed_values: raise ValueError( "Invalid value for `backend_mode` ({0}), must be one of {1}" # noqa: E501 diff --git a/specs/zrok.yml b/specs/zrok.yml index 275e7a73..5986c003 100644 --- a/specs/zrok.yml +++ b/specs/zrok.yml @@ -973,7 +973,7 @@ definitions: type: string backendMode: type: string - enum: ["proxy", "web", "tcpTunnel", "udpTunnel", "caddy", "drive"] + enum: ["proxy", "web", "tcpTunnel", "udpTunnel", "caddy", "drive", "socks"] backendProxyEndpoint: type: string authScheme: From 7f66489270fd805c4acedfd49d289df862352582 Mon Sep 17 00:00:00 2001 From: Michael Quigley Date: Thu, 8 Feb 2024 13:20:26 -0500 Subject: [PATCH 3/9] fix for sqlite migration (#558) --- controller/store/sql/sqlite3/017_v0_4_24_backend_mode_socks.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/controller/store/sql/sqlite3/017_v0_4_24_backend_mode_socks.sql b/controller/store/sql/sqlite3/017_v0_4_24_backend_mode_socks.sql index cfa65fab..20d50b82 100644 --- a/controller/store/sql/sqlite3/017_v0_4_24_backend_mode_socks.sql +++ b/controller/store/sql/sqlite3/017_v0_4_24_backend_mode_socks.sql @@ -22,6 +22,7 @@ create table shares ( constraint chk_backend_mode check (backend_mode == 'proxy' or backend_mode == 'web' or backend_mode == 'tcpTunnel' or backend_mode == 'udpTunnel' or backend_mode == 'caddy' or backend_mode == 'drive' or backend_mode == 'socks') ); insert into shares select * from shares_old; +drop index shares_token_idx; create unique index shares_token_idx ON shares(token) WHERE deleted is false; alter table frontends rename to frontends_old; From 6440685b25e0eda0ce1d00de4e2a6e30b6be48ae Mon Sep 17 00:00:00 2001 From: Michael Quigley Date: Thu, 8 Feb 2024 13:39:33 -0500 Subject: [PATCH 4/9] cli scaffolding for the socks backend (#558) --- cmd/zrok/accessPrivate.go | 22 ++++++++++++++++++++++ cmd/zrok/reserve.go | 9 ++++++--- cmd/zrok/sharePrivate.go | 27 ++++++++++++++++++++++++++- cmd/zrok/shareReserved.go | 22 ++++++++++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/cmd/zrok/accessPrivate.go b/cmd/zrok/accessPrivate.go index e478c8a1..8bb1402f 100644 --- a/cmd/zrok/accessPrivate.go +++ b/cmd/zrok/accessPrivate.go @@ -143,6 +143,28 @@ func (cmd *accessPrivateCommand) run(_ *cobra.Command, args []string) { } }() + case "socks": + fe, err := tcpTunnel.NewFrontend(&tcpTunnel.FrontendConfig{ + BindAddress: cmd.bindAddress, + IdentityName: env.EnvironmentIdentityName(), + ShrToken: args[0], + RequestsChan: requests, + }) + if err != nil { + if !panicInstead { + tui.Error("unable to create private access", err) + } + panic(err) + } + go func() { + if err := fe.Run(); err != nil { + if !panicInstead { + tui.Error("error starting access", err) + } + panic(err) + } + }() + default: cfg := proxy.DefaultFrontendConfig(env.EnvironmentIdentityName()) cfg.ShrToken = shrToken diff --git a/cmd/zrok/reserve.go b/cmd/zrok/reserve.go index cb1ae2d8..94695827 100644 --- a/cmd/zrok/reserve.go +++ b/cmd/zrok/reserve.go @@ -38,7 +38,7 @@ func newReserveCommand() *reserveCommand { command := &reserveCommand{cmd: cmd} cmd.Flags().StringVarP(&command.uniqueName, "unique-name", "n", "", "A unique name for the reserved share (defaults to generated identifier)") cmd.Flags().StringArrayVar(&command.frontendSelection, "frontends", []string{"public"}, "Selected frontends to use for the share") - cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode (public|private: proxy, web, caddy, drive) (private: tcpTunnel, udpTunnel)") + cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode (public|private: proxy, web, caddy, drive) (private: tcpTunnel, udpTunnel, socks)") cmd.Flags().BoolVarP(&command.jsonOutput, "json-output", "j", false, "Emit JSON describing the created reserved share") cmd.Flags().StringArrayVar(&command.basicAuth, "basic-auth", []string{}, "Basic authentication users (,...)") cmd.Flags().StringVar(&command.oauthProvider, "oauth-provider", "", "Enable OAuth provider [google, github]") @@ -52,7 +52,7 @@ func newReserveCommand() *reserveCommand { func (cmd *reserveCommand) run(_ *cobra.Command, args []string) { shareMode := sdk.ShareMode(args[0]) - privateOnlyModes := []string{"tcpTunnel", "udpTunnel"} + privateOnlyModes := []string{"tcpTunnel", "udpTunnel", "socks"} if shareMode != sdk.PublicShareMode && shareMode != sdk.PrivateShareMode { tui.Error("invalid sharing mode; expecting 'public' or 'private'", nil) } else if shareMode == sdk.PublicShareMode && slices.Contains(privateOnlyModes, cmd.backendMode) { @@ -87,8 +87,11 @@ func (cmd *reserveCommand) run(_ *cobra.Command, args []string) { case "drive": target = args[1] + case "socks": + target = "" + default: - tui.Error(fmt.Sprintf("invalid backend mode '%v'; expected {proxy, web, tcpTunnel, udpTunnel, caddy, drive}", cmd.backendMode), nil) + tui.Error(fmt.Sprintf("invalid backend mode '%v'; expected {proxy, web, tcpTunnel, udpTunnel, caddy, drive, socks}", cmd.backendMode), nil) } env, err := environment.LoadRoot() diff --git a/cmd/zrok/sharePrivate.go b/cmd/zrok/sharePrivate.go index 962b5cdd..291d4849 100644 --- a/cmd/zrok/sharePrivate.go +++ b/cmd/zrok/sharePrivate.go @@ -6,6 +6,7 @@ import ( "github.com/openziti/zrok/endpoints" "github.com/openziti/zrok/endpoints/drive" "github.com/openziti/zrok/endpoints/proxy" + "github.com/openziti/zrok/endpoints/socks" "github.com/openziti/zrok/endpoints/tcpTunnel" "github.com/openziti/zrok/endpoints/udpTunnel" "github.com/openziti/zrok/environment" @@ -39,7 +40,7 @@ func newSharePrivateCommand() *sharePrivateCommand { } command := &sharePrivateCommand{cmd: cmd} cmd.Flags().StringArrayVar(&command.basicAuth, "basic-auth", []string{}, "Basic authentication users (,...") - cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode {proxy, web, tcpTunnel, udpTunnel, caddy, drive}") + cmd.Flags().StringVarP(&command.backendMode, "backend-mode", "b", "proxy", "The backend mode {proxy, web, tcpTunnel, udpTunnel, caddy, drive, socks}") cmd.Flags().BoolVar(&command.headless, "headless", false, "Disable TUI and run headless") cmd.Flags().BoolVar(&command.insecure, "insecure", false, "Enable insecure TLS certificate validation for ") cmd.Run = command.run @@ -76,6 +77,9 @@ func (cmd *sharePrivateCommand) run(_ *cobra.Command, args []string) { case "drive": target = args[0] + case "socks": + target = "" + default: tui.Error(fmt.Sprintf("invalid backend mode '%v'; expected {proxy, web, tcpTunnel, udpTunnel, caddy, drive}", cmd.backendMode), nil) } @@ -264,6 +268,27 @@ func (cmd *sharePrivateCommand) run(_ *cobra.Command, args []string) { } }() + case "socks": + cfg := &socks.BackendConfig{ + IdentityPath: zif, + ShrToken: shr.Token, + Requests: requests, + } + + be, err := socks.NewBackend(cfg) + if err != nil { + if !panicInstead { + tui.Error("error creating socks backend", err) + } + panic(err) + } + + go func() { + if err := be.Run(); err != nil { + logrus.Errorf("error running socks backend: %v", err) + } + }() + default: tui.Error("invalid backend mode", nil) } diff --git a/cmd/zrok/shareReserved.go b/cmd/zrok/shareReserved.go index b3c20d84..580ac3a4 100644 --- a/cmd/zrok/shareReserved.go +++ b/cmd/zrok/shareReserved.go @@ -7,6 +7,7 @@ import ( "github.com/openziti/zrok/endpoints" "github.com/openziti/zrok/endpoints/drive" "github.com/openziti/zrok/endpoints/proxy" + "github.com/openziti/zrok/endpoints/socks" "github.com/openziti/zrok/endpoints/tcpTunnel" "github.com/openziti/zrok/endpoints/udpTunnel" "github.com/openziti/zrok/environment" @@ -258,6 +259,27 @@ func (cmd *shareReservedCommand) run(_ *cobra.Command, args []string) { } }() + case "socks": + cfg := &socks.BackendConfig{ + IdentityPath: zif, + ShrToken: shrToken, + Requests: requests, + } + + be, err := socks.NewBackend(cfg) + if err != nil { + if !panicInstead { + tui.Error("error creating socks backend", err) + } + panic(err) + } + + go func() { + if err := be.Run(); err != nil { + logrus.Errorf("error running socks backend: %v", err) + } + }() + default: tui.Error("invalid backend mode", nil) } From 3afbb6dc9ac97bb65842c71e5fecad17f2ad1790 Mon Sep 17 00:00:00 2001 From: Michael Quigley Date: Thu, 8 Feb 2024 13:56:38 -0500 Subject: [PATCH 5/9] no target for socks backend (#558) --- cmd/zrok/reserve.go | 26 +++++++++++++++++++++++--- cmd/zrok/sharePrivate.go | 26 +++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/cmd/zrok/reserve.go b/cmd/zrok/reserve.go index 94695827..519e3ddf 100644 --- a/cmd/zrok/reserve.go +++ b/cmd/zrok/reserve.go @@ -31,9 +31,9 @@ type reserveCommand struct { func newReserveCommand() *reserveCommand { cmd := &cobra.Command{ - Use: "reserve ", + Use: "reserve []", Short: "Create a reserved share", - Args: cobra.ExactArgs(2), + Args: cobra.RangeArgs(1, 2), } command := &reserveCommand{cmd: cmd} cmd.Flags().StringVarP(&command.uniqueName, "unique-name", "n", "", "A unique name for the reserved share (defaults to generated identifier)") @@ -66,6 +66,9 @@ func (cmd *reserveCommand) run(_ *cobra.Command, args []string) { var target string switch cmd.backendMode { case "proxy": + if len(args) != 2 { + tui.Error("the 'proxy' backend mode expects a ", nil) + } v, err := parseUrl(args[1]) if err != nil { tui.Error("invalid target endpoint URL", err) @@ -73,22 +76,39 @@ func (cmd *reserveCommand) run(_ *cobra.Command, args []string) { target = v case "web": + if len(args) != 2 { + tui.Error("the 'web' backend mode expects a ", nil) + } target = args[1] case "tcpTunnel": + if len(args) != 2 { + tui.Error("the 'tcpTunnel' backend mode expects a ", nil) + } target = args[1] case "udpTunnel": + if len(args) != 2 { + tui.Error("the 'udpTunnel' backend mode expects a ", nil) + } target = args[1] case "caddy": + if len(args) != 2 { + tui.Error("the 'caddy' backend mode expects a ", nil) + } target = args[1] case "drive": + if len(args) != 2 { + tui.Error("the 'drive' backend mode expects a ", nil) + } target = args[1] case "socks": - target = "" + if len(args) != 1 { + tui.Error("the 'socks' backend mode does not expect ", nil) + } default: tui.Error(fmt.Sprintf("invalid backend mode '%v'; expected {proxy, web, tcpTunnel, udpTunnel, caddy, drive, socks}", cmd.backendMode), nil) diff --git a/cmd/zrok/sharePrivate.go b/cmd/zrok/sharePrivate.go index 291d4849..7c01e31a 100644 --- a/cmd/zrok/sharePrivate.go +++ b/cmd/zrok/sharePrivate.go @@ -34,9 +34,9 @@ type sharePrivateCommand struct { func newSharePrivateCommand() *sharePrivateCommand { cmd := &cobra.Command{ - Use: "private ", + Use: "private []", Short: "Share a target resource privately", - Args: cobra.ExactArgs(1), + Args: cobra.RangeArgs(0, 1), } command := &sharePrivateCommand{cmd: cmd} cmd.Flags().StringArrayVar(&command.basicAuth, "basic-auth", []string{}, "Basic authentication users (,...") @@ -52,6 +52,9 @@ func (cmd *sharePrivateCommand) run(_ *cobra.Command, args []string) { switch cmd.backendMode { case "proxy": + if len(args) != 1 { + tui.Error("the 'proxy' backend mode expects a ", nil) + } v, err := parseUrl(args[0]) if err != nil { if !panicInstead { @@ -62,23 +65,40 @@ func (cmd *sharePrivateCommand) run(_ *cobra.Command, args []string) { target = v case "web": + if len(args) != 1 { + tui.Error("the 'web' backend mode expects a ", nil) + } target = args[0] case "tcpTunnel": + if len(args) != 1 { + tui.Error("the 'tcpTunnel' backend mode expects a ", nil) + } target = args[0] case "udpTunnel": + if len(args) != 1 { + tui.Error("the 'udpTunnel' backend mode expects a ", nil) + } target = args[0] case "caddy": + if len(args) != 1 { + tui.Error("the 'caddy' backend mode expects a ", nil) + } target = args[0] cmd.headless = true case "drive": + if len(args) != 1 { + tui.Error("the 'drive' backend mode expects a ", nil) + } target = args[0] case "socks": - target = "" + if len(args) != 0 { + tui.Error("the 'socks' backend mode does not expect ", nil) + } default: tui.Error(fmt.Sprintf("invalid backend mode '%v'; expected {proxy, web, tcpTunnel, udpTunnel, caddy, drive}", cmd.backendMode), nil) From 7380a907d2468f4c8e684f135ab4e2b06594b991 Mon Sep 17 00:00:00 2001 From: Michael Quigley Date: Thu, 8 Feb 2024 14:05:29 -0500 Subject: [PATCH 6/9] support for request logging in the socks implementation (#558) --- endpoints/socks/backend.go | 2 +- endpoints/socks/socks5.go | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/endpoints/socks/backend.go b/endpoints/socks/backend.go index 1ad152b4..44229763 100644 --- a/endpoints/socks/backend.go +++ b/endpoints/socks/backend.go @@ -41,7 +41,7 @@ func NewBackend(cfg *BackendConfig) (*Backend, error) { return &Backend{ cfg: cfg, listener: listener, - server: &Server{}, + server: &Server{Requests: cfg.Requests}, }, nil } diff --git a/endpoints/socks/socks5.go b/endpoints/socks/socks5.go index 8b1d1593..28a465d2 100755 --- a/endpoints/socks/socks5.go +++ b/endpoints/socks/socks5.go @@ -16,6 +16,7 @@ import ( "context" "encoding/binary" "fmt" + "github.com/openziti/zrok/endpoints" "github.com/sirupsen/logrus" "io" "net" @@ -86,6 +87,9 @@ type Server struct { // Username and Password, if set, are the credential clients must provide. Username string Password string + + // For notifying user-facing components about activity + Requests chan *endpoints.Request } func (s *Server) dial(ctx context.Context, network, addr string) (net.Conn, error) { @@ -171,6 +175,14 @@ func (c *Conn) handleRequest() error { } c.request = req + if c.srv.Requests != nil { + c.srv.Requests <- &endpoints.Request{ + Stamp: time.Now(), + Method: "CONNECT", + Path: fmt.Sprintf("%v:%d", c.request.destination, c.request.port), + } + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() srv, err := c.srv.dial( From 1ff2b3ea2f24e5f3051a953571ab50c0c89f77be Mon Sep 17 00:00:00 2001 From: Michael Quigley Date: Thu, 8 Feb 2024 14:08:03 -0500 Subject: [PATCH 7/9] acknowledgements (#558) --- ACKNOWLEDGEMENTS.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 8221d3c4..690eacb9 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -1,5 +1,38 @@ # ACKNOWLEDGEMENTS +## github.com/openziti/zrok/endpoints/socks + +Portions of the `socks` package is based on code from `https://github.com/tailscale/tailscale/blob/v1.58.2/net/socks5/socks5.go`, which included the following license: + +> BSD 3-Clause License +> +> Copyright (c) 2020 Tailscale Inc & AUTHORS. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions are met: +> +> 1. Redistributions of source code must retain the above copyright notice, this +> list of conditions and the following disclaimer. +> +> 2. Redistributions in binary form must reproduce the above copyright notice, +> this list of conditions and the following disclaimer in the documentation +> and/or other materials provided with the distribution. +> +> 3. Neither the name of the copyright holder nor the names of its +> contributors may be used to endorse or promote products derived from +> this software without specific prior written permission. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ## github.com/openziti/zrok/drives/davServer The `davServer` package is based on code from `https://cs.opensource.google/go/go/`, which included the following license: From 2982e4368570bad00e9d4217e0ffecd2893508d9 Mon Sep 17 00:00:00 2001 From: Michael Quigley Date: Thu, 8 Feb 2024 14:11:35 -0500 Subject: [PATCH 8/9] changelog (#558) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e1ed219..256f193f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## v0.4.24 +FEATURE: New `socks` backend mode for use with private sharing. Use `zrok share private --backend-mode socks` and then `zrok access private` that share from somewhere else... very lightweight VPN-like functionality (https://github.com/openziti/zrok/issues/558) + FEATURE: New `zrok admin create account` command that allows populating accounts directly into the underlying controller database (https://github.com/openziti/zrok/issues/551) CHANGE: The `zrok test loopback public` utility to report non-`200` errors and also ensure that the listening side of the test is fully established before starting loopback testing. From d58779c7e28fdbba553e4dfd3fa840f7b5e16bf4 Mon Sep 17 00:00:00 2001 From: Michael Quigley Date: Thu, 8 Feb 2024 21:46:50 -0500 Subject: [PATCH 9/9] no need to update targets when reserved backend mode is 'socks' (#558) --- cmd/zrok/shareReserved.go | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/cmd/zrok/shareReserved.go b/cmd/zrok/shareReserved.go index 580ac3a4..8275f198 100644 --- a/cmd/zrok/shareReserved.go +++ b/cmd/zrok/shareReserved.go @@ -93,23 +93,25 @@ func (cmd *shareReservedCommand) run(_ *cobra.Command, args []string) { panic(err) } - logrus.Infof("sharing target: '%v'", target) + if resp.Payload.BackendMode != "socks" { + logrus.Infof("sharing target: '%v'", target) - if resp.Payload.BackendProxyEndpoint != target { - upReq := share.NewUpdateShareParams() - upReq.Body = &rest_model_zrok.UpdateShareRequest{ - ShrToken: shrToken, - BackendProxyEndpoint: target, - } - if _, err := zrok.Share.UpdateShare(upReq, auth); err != nil { - if !panicInstead { - tui.Error("unable to update backend proxy endpoint", err) + if resp.Payload.BackendProxyEndpoint != target { + upReq := share.NewUpdateShareParams() + upReq.Body = &rest_model_zrok.UpdateShareRequest{ + ShrToken: shrToken, + BackendProxyEndpoint: target, } - panic(err) + if _, err := zrok.Share.UpdateShare(upReq, auth); err != nil { + if !panicInstead { + tui.Error("unable to update backend proxy endpoint", err) + } + panic(err) + } + logrus.Infof("updated backend proxy endpoint to: %v", target) + } else { + logrus.Infof("using existing backend proxy endpoint: %v", target) } - logrus.Infof("updated backend proxy endpoint to: %v", target) - } else { - logrus.Infof("using existing backend proxy endpoint: %v", target) } var shareDescription string