Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SOAX relay for remote measurement #286

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open

SOAX relay for remote measurement #286

wants to merge 17 commits into from

Conversation

amircybersec
Copy link
Contributor

This PR adds an example for running a socks5 proxy that forwards requests to soax server socks5 server. It helps with temporarily sharing access with others to let them perform measurements on a given network.


The relay will use the SOAX package ID and password to connect to the SOAX server, appending the country, sessionid, and sessionlength from the user's input.

### Setup and Running
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too complicated. You can just use go run directly and pass the full package name. See the docs for the other tools.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I simplified the readme accordingly


The relay will forward these parameters to SOAX, allowing users to benefit from sticky sessions without direct access to SOAX credentials.

### Security Considerations
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, SOCKS5 is plaintext....

Comment on lines 27 to 29
server:
address: "127.0.0.1"
port: "1080"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"server" is ambiguous. Perhaps "listen_address"? And we can simplify with a single string

Suggested change
server:
address: "127.0.0.1"
port: "1080"
listen_address: 127.0.0.1:1080

Comment on lines 31 to 34
upstream:
prefix: "package-xxxxx-"
password: "YourSOAXPassword"
address: "proxy.soax.com:5000"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's simplify this and be explicit about the soax info, using the names they use:

Suggested change
upstream:
prefix: "package-xxxxx-"
password: "YourSOAXPassword"
address: "proxy.soax.com:5000"
soax:
package_id: "XXXXXX"
package_key: "YourSOAXPassword"

I'd omit the server address too, it's easier.

foo: "bar"
jane: "password123"

udp_timeout: 1m
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
udp_timeout: 1m
# Optional
udp_timeout: 1m

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout param is removed

viper.SetConfigType("yaml")
viper.AddConfigPath(".")

if err := viper.ReadInConfig(); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No globals please. This hurts readability and maintainability. Can you change the code so it doesn't rely on globals? You can use https://pkg.go.dev/github.com/dvln/viper#New instead.


server := socks5.NewServer(
socks5.WithAuthMethods([]socks5.Authenticator{customAuth}),
socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not required, but you could use slog with https://pkg.go.dev/log/slog#NewLogLogger


switch network {
case "tcp", "tcp4", "tcp6":
streamEndpoint := transport.StreamDialerEndpoint{Dialer: &transport.TCPDialer{}, Address: soaxAddress}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create the endpoint outside the handler. You only need to create it once, no need to create it for every request

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am going to move the socks5 client creation outside of the handler as well.

}

// Valid checks if the provided prefix and password are valid
func (s *StaticCredentialStore) Valid(prefix, password, userAddr string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefix -> username

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate is now removed entirely

}

// CredentialStore is an interface to validate prefix-based credentials
type CredentialStore interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many layers here. I don't see much value in the CredentialStore interface and the StaticCredentialStore. The CustomCredentialAuthenticator can hold the authentication logic. It's an integral part of the authenticator.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed these extra layers and did a simple map lookup inside Authenticator

foo: bar
exampleuser: examplepass

udp_timeout: 1m
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UDP timeout is not needed. Remove.

The TCP connection for the UDP Association is how we determine when to close it.

}

// Start a goroutine to close the connection after a timeout
go func() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace this timeout with a loop that reads from the client TCP connection and closes the target TCP connection and the client UDP connection when the client TCP connection is closed. The "loop" can be an io.Copy directly into io.Discard.

// Construct the upstream username by concatenating the base username with the suffix
soaxUsername := soaxpackage + suffix // e.g., "package-189365-country-ir-seesionid-..."

switch network {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle UDP here as well. There's no need to implement an AssociateHandler, since the library already provides that.

return exists && storedPassword == password
}

func udpAssociateHandler(ctx context.Context, writer io.Writer, request *socks5.Request) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this and handle UDP in the Server dialer instead.

I'm not comfortable exposing the internals of our SOCKS5 library for now and that will require some more thought.

Using the dialer approach will keep the code simpler, even though it's doing an extra relay of the packets.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fortuna this is an important part of the implementation. I am basically implementing a custom udpAssociateHandler which relays the UDP associate command coming from the client (to proxy server) to the upstream soax server and relays the bind address communicated by soax server back to the client.

I initially thought I can use WithDialAndRequest to relay UDP as well but this handler only handle outgoing TCP connections (and not even the UDP associate TCP connection). I guess you are suggesting that I use a packetDialer similar to SteamDialer in the WithDialAndRequest for udp networks right? If so, unfortunately that approach won't work since WithDialAndRequest won't be called for udp.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The library already implements the relay:
https://github.com/things-go/go-socks5/blob/503e7866821a419fe3766751f6d5671390072c14/handle.go#L219

The dial to the target happens here:
https://github.com/things-go/go-socks5/blob/503e7866821a419fe3766751f6d5671390072c14/handle.go#L242

It's directly from Server.dial:
https://github.com/things-go/go-socks5/blob/503e7866821a419fe3766751f6d5671390072c14/handle.go#L183

Which is set by WithDialAndRequest. That applies to both tcp and udp.

You will need to take the credentials from the request, create the SOCKS5 dialer, then dial.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah no! It doesn't actually use Server.dialWithRequest!
So, yeah, we need to provide the relay.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert the transport changes. See below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fortuna please refer to my above comment.

}

streamEndpoint := transport.StreamDialerEndpoint{Dialer: &transport.TCPDialer{}, Address: config.SOAX.Address}
client, err := csocks5.NewClient(&streamEndpoint)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't reuse the client, because you need to set the credentials for each dial. You now have a race condition.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fortuna these two lines were actually inside WithDialAndRequest (so every dial will override the credentials for that dial). I need to revert back.


// Construct the upstream username by concatenating the base username with the suffix
soaxUsername := config.SOAX.PackageID + suffix // e.g., "package-123456-country-ir-seesionid-..."
err = client.SetCredentials([]byte(soaxUsername), []byte(config.SOAX.PackageKey))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a race condition, since it will change the credential for everyone using the client

// the soax package name with the suffix
soaxUsername := config.SOAX.PackageID + suffix

streamEndpoint := transport.StreamDialerEndpoint{Dialer: &transport.TCPDialer{}, Address: config.SOAX.Address}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reuse the streamEndpoint with TCP to make it clear it's the same and that it doesn't change on every request.

}

server := socks5.NewServer(
socks5.WithAuthMethods([]socks5.Authenticator{customAuth}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New Approach:

  • Let's use WithAssociateMiddleware.
    • We have access to the Request there, so we can process the credentials and create the SOCKS5 client, which we can store in the Context
    • This is not released yet, so you will need to depend on a specific commit.
  • In the Dial function (specified with WithDial), retrieve the client from the Context and dial.

@@ -3,7 +3,7 @@ module github.com/Jigsaw-Code/outline-sdk/x
go 1.21

require (
github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240726212635-470a9290ec57
github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240920003233-01a5556214fd
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's change this to version 0.0.17 and re-run go mod tidy. I believe we can revert the change to the socks5 package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fortuna I will get back to exploring and implementing the suggested alternative approach in a bit. That would be a requirement for reverting the changes to socks5 package (which exposes method to send associate command). I am right now focusing on getting a set of remote measurements for the upcoming event and get back on this PR afterwards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants