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

Support for listening on Unix sockets #545

Closed
pzmarzly opened this issue Jan 20, 2018 · 19 comments
Closed

Support for listening on Unix sockets #545

pzmarzly opened this issue Jan 20, 2018 · 19 comments
Labels
request Request for new functionality

Comments

@pzmarzly
Copy link

I would like to utilize my existing infrastructure on atthost.pl. They are running nginx proxy and allowing me to bind it to any service exposing Unix socket (so I can run servers written in Nodejs, RoR, Golang etc.). The proxy handles SSL/TLS and communicates with servers through socket file. I cannot f.e. open a random port and use socat as proxy between TCP and Unix sockets (due to missing netstat command).

Rust std has UnixListener, but hyper 0.10.x does not. Should anyone find time to implement this feature, here is some relevant code:

@SergioBenitez SergioBenitez added the request Request for new functionality label Jan 20, 2018
@pzmarzly
Copy link
Author

This may also allow for cleaner way to run Rocket as CGI (issues #132 and #97).

@piperRyan
Copy link

@SergioBenitez I would be interested in taking this issue on. I will try to get this done by the end of the reading week.

@SergioBenitez
Copy link
Member

@piperRyan Sure, that would be great! Before you start writing code, please post a detailed description of your design plans. In particular, you should address how you're going to tackle this problem internally and how the new functionality will be exposed to the users. The former can be somewhat vague but the latter should be crystal clear. You should include example code and/or configuration files. This also gives you an opportunity to post any questions you might have about how to approach this.

I'll then review the plan and suggest changes, if any, that will make it more likely for your contribution to be accepted without any major changes.

@piperRyan
Copy link

piperRyan commented Feb 18, 2018

@SergioBenitez unfortunately, some stuff came up so I may not be able to hit my desired target. However, this is a early formation of my plan. I still want to research some other implementation to see how they are handling this case. That being said I would still appreciate some feedback.

Files Expected Change

  • test.sh
  • test module
  • Config Module
    • build.rs
    • config.rs
    • custom_values.rs
  • Data Module
    • data.rs
    • data_stream.rs
    • net_stream.rs
  • build.rs?

General Design Plan

  • Leverage hyperlocal 0.3.0 which uses 0.10.3 hyper. Downside of using this that no logging can be added to it. Maybe it would work as part of throwaway prototype and if the overall design is accepted we can do a in project solution.
  • Sock files would be propagated, but as for cleaning up there is no solid answer right now. We can use chan-signal to handle unix signals. Otherwise, the only I can think of is we can use is if files exists delete it and recreates the sock file. Alternatively, we can use a .lock file and reuse the sock file but I find that to be a weak solution or we can clearly state it is the user responsibility to handle its clean up and give a systemd example.
  • My intention is have the same logic for tcp/http servers and similar logic for unix socket creation. So on high level I will be moving logic that has unix socket counterpart into a separate module, but nested within the module it is in right now. For example, I would create builders module then mv build.rs to tcp_build.rs and create a unix_build.rs file and update the mod.rs file so that when the feature used the BuildConfig is still the same struct used regardless despite the different fields.
  • I suspect that the overall logic will be similar so I am not going into depth in that regard.

How this will exposed to the end user

  • I think using a feature is the cleanest answer so:
cargo run --features unix_handler

will generate the socket file, but pointing any traffic should be left to the user as it depend fairly widely on how the end user wants to do so be it with nginx, netstat, socat or whatever.

  • As for configuration details I was thinking
#[derive(Clone)]
pub struct ConfigBuilder {
    /// The environment that this configuration corresponds to. 
    pub environment: Environment,
    /// The path to create the socket.
    pub socket_path: Path,
    /// The number of workers to run in parallel.
    pub workers: u16,
    /// How much information to log.
    pub log_level: LoggingLevel,
    /// Size limits.
    pub limits: Limits,
    /// Any extra parameters that aren't part of Rocket's config.
    pub extras: HashMap<String, Value>,
    /// The root directory of this config.
    pub root: PathBuf,
}

impl ConfigBuilder  {
   pub fn new(environment: Environment) -> ConfigBuilder { // ... }
   pub fn socket_path<P: <AsRef<Path>>>(mut self, socket_path: P) -> Self {  //... }
   pub fn workers(mut self, workers: u16) -> Self { // ... }
   pub fn log_level(mut self, log_level: LoggingLevel) -> Self { // ... }
   pub fn limits(mut self, limits: Limits) -> Self { // ... }
   pub fn environment(mut self, env: Environment) -> Self { // ... } 
   pub fn root<P: AsRef<Path>>(mut self, path: P) -> Self  { // ... } 
   pub fn extra<V: Into<Value>>(mut self, name: &str, value: V) -> Self { // ... } 
   pub fn finalize(self) -> Result<Config> { // ... } 
   pub fn unwrap(self) -> Config { // ... } 
   pub fn expect(self, msg: &str) -> Config { // ... } 
}

The same thought process for the Config setup:

#[derive(Clone)]
pub struct Config {
    /// The environment that this configuration corresponds to.
    pub environment: Environment,
    /// The path to the socket.
    pub socket_path: Path,
    /// The number of workers to run concurrently.
    pub workers: u16,
    /// How much information to log.
    pub log_level: LoggingLevel,
    /// Streaming read size limits.
    pub limits: Limits,
    /// Extra parameters that aren't part of Rocket's core config.
    pub extras: HashMap<String, Value>,
    /// The path to the configuration file this config belongs to.
    pub config_path: PathBuf,
}

impl Config {
    pub fn build(env: Environment) -> ConfigBuilder { // ... } 
    pub fn new(env: Environment) -> Result<Config> { // ... }
    pub fn development() -> Result<Config> { // ... }
    pub fn staging() -> Result<Config> { // ... }
    pub fn production() -> Result<Config> { // ... } 
    pub(crate) fn default<P>(env: Environment, path: P) -> Result<Config> { // ... }  
    pub(crate) fn bad_type(&self,name: &str, actual: &'static str, expect: &'static str) -> ConfigError { // ... }
    pub(crate) fn set_raw(&mut self, name: &str, val: &Value) -> Result<()> {// .... }
    pub fn set_root<P: AsRef<Path>>(&mut self, path: P) { // .... }
    pub fn set_socket_path<P: AsRef<Path>>(&mut self, socket_path: P) -> Result<()> { // ...  }
     pub fn set_workers(&mut self, workers: u16) { // ... }
     pub fn set_log_level(&mut self, log_level: LoggingLevel) { // ... } 
     pub fn set_limits(&mut self, limits: Limits) { // ... }
    pub fn set_extras(&mut self, extras: HashMap<String, Value>) { // ... }
    pub fn extras<'a>(&'a self) -> impl Iterator<Item=(&'a str, &'a Value)> { // ...    }
    pub fn get_str<'a>(&'a self, name: &str) -> Result<&'a str> {}
    pub fn get_string(&self, name: &str) -> Result<String> {// ... }
    pub fn get_int(&self, name: &str) -> Result<i64> { // ...  }
    pub fn get_bool(&self, name: &str) -> Result<bool> { // ... }
    pub fn get_float(&self, name: &str) -> Result<f64> { // ... }
    pub fn get_slice(&self, name: &str) -> Result<&Array> {// ... }
    pub fn get_table(&self, name: &str) -> Result<&Table> { // ... }
    pub fn get_datetime(&self, name: &str) -> Result<&Datetime> { //...  }
    pub fn root(&self) -> &Path { // ... }
    pub fn root_relative<P: AsRef<Path>>(&self, path: P) -> PathBuf {// ... }

impl fmt::Debug for Config {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // ... }
}

impl PartialEq for Config {
    fn eq(&self, other: &Config) -> bool { // ... }
}

So my intention here is to have the syntax/flow as similar as possible. However, it might change a bit depending how socket creation/cleanup is done.

  • The tcp/http server logic will set up as default feature, but we can also set it up so the tcp logic is a not tls and not unix feature to keep it similar.
  • I think that is all that is exposed to the end user please let me know if that is not the case.

@SergioBenitez
Copy link
Member

SergioBenitez commented Feb 19, 2018

@piperRyan Thanks for the write-up!

Leverage hyperlocal 0.3.0 which uses 0.10.3 hyper. Downside of using this that no logging can be added to it. Maybe it would work as part of throwaway prototype and if the overall design is accepted we can do a in project solution.

For the most part, using hyperlocal seems okay, but I'm concerned about a couple of things:

  1. hyperlocal 0.3.0 depends on rustc-serialize which is deprecated.
  2. hyperlocal 0.3.0 is, understandably, unmaintained.
  3. hyperlocal brings in the client side of hyper, an unnecessary dependency

My take is that it would be easier and more maintainable to do what hyperlocal does ourselves. It's licensed under MIT so we can easily bring in the code from hyperlocal if we need to. For a proof of concept, however, I'd be fine with a hyperlocal dependency.

Also, I don't think any of the logging stuff should be an issue. What potential issues do you see there?

Sock files would be propagated, but as for cleaning up there is no solid answer right now. We can use chan-signal to handle unix signals. Otherwise, the only I can think of is we can use is if files exists delete it and recreates the sock file. Alternatively, we can use a .lock file and reuse the sock file but I find that to be a weak solution or we can clearly state it is the user responsibility to handle its clean up and give a systemd example.

This is a great point, and I'm happy to see you're thinking about it. I think the most natural thing for Rocket to do is to fail on start-up if a socket already exists, create a socket on start-up if it doesn't, and delete the socket on shut-down. This depends on #180, of course, but I would love to see that solved as well. I think leaving unused socket files around is a bad idea, and we shouldn't roll this out unless we can clean up after ourselves. As a proof-of-concept, I'd be okay leaving this out. But it would need to be present for a PR to be merged.

My intention is have the same logic for tcp/http servers and similar logic for unix socket creation. So on high level I will be moving logic that has unix socket counterpart into a separate module, but nested within the module it is in right now. For example, I would create builders module then mv build.rs to tcp_build.rs and create a unix_build.rs file and update the mod.rs file so that when the feature used the BuildConfig is still the same struct used regardless despite the different fields.

I don't think sockets should be behind a feature flag. If you're compiling on a platform that supports them (by the way, presumably none of this will work on Windows, right?), you should have the option. The code cost to support them shouldn't be high, so a feature flag just adds friction.

How this will exposed to the end user [..]

You've proposed adding a socket_path field to the config. One complication is that a user can write:

[development]
socket_path = "/my/socket"
address = "0.0.0.0"

That is, specify both an address and a socket_path. I think it'd be a better approach to instead allow the user to specify a socket path in the address field. Something like:

[development]
address = "unix:///my/socket"

If we do make this change, we probably want to also allow:

[development]
address = "0.0.0.0" # serve over TCP by default
address = "tcp://0.0.0.0" # serve over TCP
address = "udp://0.0.0.0" # serve over UDP...if a user wants it

Then specifying port with a unix:// address becomes an error:

[development]
address = "unix:///my/socket"
port = 8000 # error: address type 'unix' does not accept a port

Also, how will using a unix domain socket interact with TLS? My intuition is that if TLS is enabled, we should do TLS over the unix domain socket. This should be easy to handle.

Finally, the big change in Rocket is likely to be in the serve macro in rocket.rs which should now handle unix domain sockets as well.

@piperRyan
Copy link

piperRyan commented Feb 20, 2018

For the most part, using hyperlocal seems okay, but I'm concerned about a couple of things:
hyperlocal 0.3.0 depends on rustc-serialize which is deprecated.

Dang, that is right I forgot about that.

Also, I don't think any of the logging stuff should be an issue. What potential issues do you see there?

I don't see a potential issue per se, but rust doesn't really have any test hooks so being able to assess the internal state of the function is a powerful tool to have for debugging. However, I could lean either way on this issue as it is very rare that debugging needs to be done from a stream/socket level if is done right.

by the way, presumably none of this will work on Windows, right?

To be honest I am not sure Windows did not to long ago announce support for unix sockets. However, I haven't put much research into that blog post. Maybe this needs to be tested? It would be a interesting development to say the least. Regardless, I would say it is safe to assume that a vast majority of individuals will not have access to unix sockets on a windows machine.

I think it'd be a better approach to instead allow the user to specify a socket path in the address field.

Yea, that is a way better idea! Perhaps a simplified Enum similar to hyper's Uri struct or the rust url's url struct would be valuable here, but I will have to think on that a bit to see if there is any value in doing so.

Also, how will using a unix domain socket interact with TLS? My intuition is that if TLS is enabled, we should do TLS over the unix domain socket. This should be easy to handle.

I agree, this is one the things I wanted to check out in different projects since a couple of them that I have looked at are suggesting using socat or using Apache or nginx in front which should handle the TLS related logic. A decision I don't really agree with, but I wanted to understand the rationale behind before committing any support to TLS. However, if you want it then I will ensure it works correctly.

I will probably start thinking the types of testing that needs to be done in addition to what is present before I start coding. I will update the post with testing matrix when I get some time.

@messense
Copy link
Contributor

@piperRyan How is your implementation going?

I have just put together a proof of concept implementation in #594

@piperRyan
Copy link

@messense unfortunately, very slowly I had a few people drop from a coarse which had group project in it so accordingly I have been super busy trying to handle that. However, I will have a look at that proof concept you submitted! Regardless, thank you for picking up my slack :P.

@david415
Copy link

I would very much like to see this issue resolved soon. My new rust project requires that we write "external plugins" that are essentially an HTTP server that listens on a Unix domain socket for HTTP requests.

@atroche
Copy link

atroche commented Sep 16, 2019

I came across this issue when trying to find a way to use catflap with Rocket in order to get cargo watch working so I can have an auto-reloading dev server.

Before this issue / PR is merged, is there another good way to achieve what I want? I suppose I could bind the server on port zero to bind on an unused high port, although that adds a little bit of cognitive overhead in the form of remembering to change the port numbers in my curls / browser visits.

@atroche
Copy link

atroche commented Sep 16, 2019

Oh, actually, got it working like this:

watchexec --exts rs,toml --restart "cargo run -- --nocapture"

as suggested here: #163 (comment)

@tim-seoss
Copy link

tim-seoss commented May 4, 2021

Possibly (I've not looked at the Rocket internals at all!), a way to resolve this (and also provide additional functionality) would be to allow Rocket to accept an already bound socket listener to be passed to it from its parent OS process in the form of an open file descriptor?

This was the jist of the "catflap" comment above, but to expand on why this might be a good idea...

This would allow for easy integration with e.g. inetd, Apple launchd, and systemd (socket activation mode), and allow for:

  • listening on unix domain sockets
  • listening on tcp sockets (including "privileged" ports on platforms which only allow certain users to listen on ports below 1024)
  • configuration of arbitrary exotic TCP socket options (like TCP keepalive settings, nodelay, or congestion control algorithm selection)
  • improved process sandboxing
  • seamless server restarts (new connections block until the replacement server is running)

Some references:
https://docs.rs/listenfd/0.3.3/listenfd/index.html
http://0pointer.de/blog/projects/socket-activation.html

@xfbs
Copy link

xfbs commented Oct 25, 2021

Supporting UNIX domain sockets would be really awesome -- they are needed to use Rocket for writing Docker plugins. Without them, we are forced to use actix-web for now.

@mattfbacon
Copy link

The PR (#594) seems to have gotten derailed in large part due to supporting Unix sockets on Windows.. why is that a priority??

I could try to make an MVP with support for Unix sockets on Unix only, if there's still interest in this feature.

@mattfbacon
Copy link

Is UDP listening important to anyone? If not I'll drop it for the MVP

@IniterWorker
Copy link

@mattfbacon I have a lot of intereset in this feature.
I would like to use it with NGINX over Unix domain.

@SergioBenitez
Copy link
Member

I want to tackle this as part of #1070 wherein the default listener knows about Unix domain sockets, for both Windows and *nix, as well as TCP. Closing this as a result.

@TyrfingMjolnir
Copy link

TyrfingMjolnir commented Aug 24, 2023

How do you measure how much overhead is gone( ping, memory foot print, and network traffic wise ) by using unix sockets opposed to loading the entire network stack?

My typical use case would be to "mount" the unix socket on a network path in a reverse proxy such as nginx.

As pr example: https://www.youtube.com/watch?v=m8ogrogKjXo&t=145s

@eirnym
Copy link

eirnym commented Oct 30, 2023

There's a benchmark to measure the difference between TCP and Unix socket on the same simple server

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