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

feat: http local proxy implementation #50

Merged
merged 7 commits into from
Aug 24, 2023

Conversation

GRbit
Copy link
Contributor

@GRbit GRbit commented Aug 16, 2023

Implementation of a local HTTP proxy that can use transport.StreamDialer interface.

This is considered useful because many applications (e.g., browsers) can use proxy servers out of the box, and local proxy servers could be used to easily wrap a secure tunnel without additional user privileges.

To demo this functionality you may run local HTTP proxy as follows:

$ KEY=ss://ENCRYPTION_KEY@HOST:PORT/
$ go run github.com/Jigsaw-Code/outline-sdk/x/local-proxy@latest -transport "$KEY" -addr localhost:54321

or, to test before merging, being in outline-sdk/x folder

KEY=ss://ENCRYPTION_KEY@HOST:PORT/
go run ./local-proxy -transport "$KEY" -addr localhost:54321

The main question to discuss is how should proxy interface look like. So far I come up with this:

type Proxy interface {
	// GetAddr returns the address of the proxy server if it is configured to listen on a specific address.
	// If address is not configured and the server is not started, it returns an empty string.
	GetAddr() string

	// StartServer starts the proxy server. If no address is specified, it will listen on a random localhost port
	StartServer(addr ...string) error

	// StopServer stops the proxy server
	StopServer() error

	// GetError returns the error that caused the proxy server to stop or nil if the server is still running or not started
	GetError() error
}

Will be happy to receive any comments

@fortuna fortuna self-requested a review August 17, 2023 20:58
Copy link
Contributor

@fortuna fortuna left a comment

Choose a reason for hiding this comment

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

Thanks a lot for your contribution. It's exciting to see the first circumvention tool by an external contributor using our sdk!

x/go.mod Outdated Show resolved Hide resolved
x/local-proxy/README.md Outdated Show resolved Hide resolved
x/local-proxy/main.go Outdated Show resolved Hide resolved
x/local-proxy/main.go Outdated Show resolved Hide resolved
x/local-proxy/access_key.go Outdated Show resolved Hide resolved
x/local-proxy/proxy/proxy.go Outdated Show resolved Hide resolved
x/local-proxy/proxy/httpproxy/handler.go Outdated Show resolved Hide resolved
@fortuna fortuna marked this pull request as ready for review August 17, 2023 21:24
x/go.mod Outdated Show resolved Hide resolved
x/local-proxy/proxy/proxy.go Outdated Show resolved Hide resolved
x/local-proxy/proxy/httpproxy/handler.go Outdated Show resolved Hide resolved

prx.address = prx.l.Addr().String()
default:
prx.l, err = net.Listen("tcp", prx.address)
Copy link
Contributor

Choose a reason for hiding this comment

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

Just pass the input address here. No need for all the logic above. The standard library already handles the different cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for noticing that.
I thought that it might be a problem if the proxy is started on :54321 because it'll not be a localhost address (like localhost:54321). But it causes problem only in case it's not a localhost address in browser settings, so in fact it's ok.

The only question I have is, maybe we should enforce listening on localhost by default? In another way, a proxy will be available to other devices on the network.

}
}()

// sleep some time to check if server failed to start
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this wait. It's a race condition, and the caller can implement it if they really want it. Also, most of the errors should happen on the Listen.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Make sense, caller can just check an error after some time, so better remove it.

But can you tell me more about race condition thing? I think I'm missing something. Isn't it ok to send a message to channel from one goroutine and read from another?

Copy link
Contributor

Choose a reason for hiding this comment

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

Imagine it takes around 100ms for the error to happen. You may or may not return the error, depending on the mood of the machine. You may get different results when running the same code multiple times.

// If address is not configured and the server is not started, it returns an empty string.
GetAddr() string
// StartServer starts the proxy server. If no address is specified, it will listen on a random localhost port
StartServer(addr string) 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 make StartServer a standalone function that returns a Proxy object. (Similar how Listen returns a Listener). That way it will never be in an invalid state.

Copy link
Contributor Author

@GRbit GRbit Aug 23, 2023

Choose a reason for hiding this comment

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

Sounds nice. But I'm not sure about "never be in an invalid state" part. So, I can see StartServer (or better StartProxy) function returning a Proxy with interface like this:

type Proxy interface {
	Addr() string // removing "Get" like in Listener
	Stop() error
	GetError() error // not sure if it should be "Error()", because it will implement an "error" interface
}

I can't come up with an example when things ca go wrong, but it seems for me that they can, and the proxy can broke for some reason. Will it be what you meant or is there something else I should do about never being in invalid state?

UPD: just in case, does this StartProxy definition looks good to you?
func StartServer(d transport.StreamDialer, addr string) (Proxy, error)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, something like that.

x/local-proxy/README.md Outdated Show resolved Hide resolved
Copy link
Contributor

@fortuna fortuna left a comment

Choose a reason for hiding this comment

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

I figure it's taking a lot more time for me to review this a bit each day with lots of context switches. I think the code is ok, I'll merge and then send another PR with the refactorings I have in mind so we can move forward more quickly,

}
}()

// sleep some time to check if server failed to start
Copy link
Contributor

Choose a reason for hiding this comment

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

Imagine it takes around 100ms for the error to happen. You may or may not return the error, depending on the mood of the machine. You may get different results when running the same code multiple times.

Copy link
Contributor

Choose a reason for hiding this comment

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

This can move to x/examples/local-proxy

"github.com/Jigsaw-Code/outline-sdk/x/local-proxy/proxy/httpproxywrapper"
)

func makeStreamDialer(transportConfig string) (transport.StreamDialer, error) {
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 now use MakeStreamDialer in x/examples/internal/config

"github.com/go-httpproxy/httpproxy"
)

func BasicAuth(usersPasswords map[string]string) func(ctx *httpproxy.Context, authType, user, pass 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.

This is never used. Delete

Comment on lines +124 to +126
if err = proxy.StopServer(); err != nil {
log.Println("Failed to stop server: ", err.Error())
}
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
if err = proxy.StopServer(); err != nil {
log.Println("Failed to stop server: ", err.Error())
}
if err = listener.Close(); err != nil {
log.Println("Failed to stop server: ", err.Error())
}

// If address is not configured and the server is not started, it returns an empty string.
GetAddr() string
// StartServer starts the proxy server. If no address is specified, it will listen on a random localhost port
StartServer(addr string) error
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, something like that.

Comment on lines +104 to +109
proxy := httpproxy.NewConnectHandler(dialer)
proxy.OnError = httpproxy.OnError

if err := proxy.StartServer(*addrFlag); err != nil {
log.Fatal("Proxy server failed")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I lost the comment. I'd like to see the code like this:

Suggested change
proxy := httpproxy.NewConnectHandler(dialer)
proxy.OnError = httpproxy.OnError
if err := proxy.StartServer(*addrFlag); err != nil {
log.Fatal("Proxy server failed")
}
listener := net.Listen("tcp", *addrFlag)
if err := http.Serve(listener, httpproxy.NewHandler(dialer)); err != nil {
log.Fatal("Proxy server failed")
}

This way it's clear what is provided by the code. Also, the caller may want to provide a different listener (e.g. using TLS).

You can stop the server by calling listener.Close()

Copy link
Contributor

Choose a reason for hiding this comment

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

We can provide convenience functions later. But we need the http.Handler building block.

Copy link
Contributor

@fortuna fortuna left a comment

Choose a reason for hiding this comment

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

Let's get this merged so we can iterate

@fortuna fortuna merged commit 605df87 into Jigsaw-Code:main Aug 24, 2023
4 checks passed
@GRbit
Copy link
Contributor Author

GRbit commented Aug 26, 2023

Thank you for your work on this and all the time you've put in. Indeed, it's incredibly difficult to review PR with such a difference in time zones.

@GRbit GRbit deleted the local-proxies-draft branch August 26, 2023 09:30
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