aatxe / irc

the irc crate – usable, async IRC for Rust
Mozilla Public License 2.0
525 stars 100 forks source link

SASL and PING timeouts #218

Open savoiringfaire opened 3 years ago

savoiringfaire commented 3 years ago

Hi!

I'm writing a bot that I'm hoping will be able to run on EC2. Unfortunately Freenode require SASL authentication from certain IP's - and the server password method of authenticating does not seem to cut it for such 'blacklisted' IP's. I started out trying to use SASL EXTERNAL authentication as mentioned in #166 with a key generated by running:

openssl req -x509 -new -newkey rsa:4096 -sha256 -days 1000 -nodes -out freenode.pem -keyout freenode.pem
openssl pkcs12 -export -out Cert.p12 -in freenode.pem -inkey freenode.pem > freenode.der

and then adding to the bottom of config.toml:

client_cert_path = "Cert.p12"
client_cert_pass = "<password set in second openssl command>"
use_tls = true

However, this didn't seem to end up with me being authenticated - and there were no logs in the output relating to sasl external auth. Changing the password does come up with an error, which leads me to conclude all of the certificate parsing logic is working OK. Looking at the code in conn.rs it all looks like a fairly opaque process, and I decided that trying to figure out how the certificate-based process worked and then get enough debug output from it to be useful likely wasn't going to be the best use of my time, so I moved on to trying to use SASL plain. As far as I can tell, there is no built-in support for plain sasl authentication, so I ended up writing my own flow to handle the authentication:

let mut client = Client::from_config(config.clone()).await?;

client.send_cap_req(&[Capability::Sasl])?;
client.send(Command::PASS(config.password().to_string()))?;
client.send(Command::NICK(config.nickname()?.to_string()))?;
client.send(Command::USER(
    config.nickname()?.to_string(),
    "0".to_owned(),
    config.nickname()?.to_string(),
))?;

...

while let Some(message) = stream.next().await.transpose()? {
    match message.command {
        Command::CAP(_, ref subcommand, _, _) => {
            if subcommand.to_str() == "ACK" {
                println!("Recieved ack for sasl");
                client.send_sasl_plain()?;
            }
        },
        Command::AUTHENTICATE(_) => {
            println!("Got signal to continue authenticating");
            client.send(Command::AUTHENTICATE(base64::encode(format!("{}\x00{}\x00{}", config.nickname()?.to_string(), config.nickname()?.to_string(), config.password().to_string()))))?;
            client.send(Command::CAP(None, "END".parse()?, None, None))?;
        },
        Command::Response(code, _) => {
            if code == Response::RPL_SASLSUCCESS {
                println!("Successfully authenticated");
                client.send(Command::CAP(None, "END".parse()?, None, None))?;
            }
        },
    }
}

which, for the most part, worked fine. The main issue with the above is that the Pinger in transport.rs sends a ping too early in the process (just before [RECV] :wolfe.freenode.net NOTICE * :*** Looking up your hostname...), and it seems that the freenode IRC server doesn't ever respond to messages sent that early - this casues a PingTimeout error. I guess the Pinger could do with holding off in much the same way the channel joining logic does and only start the process once the MOTD has either been sent or ERR_NOMOTD has been received - however since the pinger currently exists in the transport layer rather than at the mod.rs level, I'm not sure what the best way of achieving such functionality would be.

I'm also wondering if it makes sense to include this PLAIN authentication method in the library somewhere. Maybe adding a sasl_plain_auth=[true|false] to the config and then adding the authentication logic to handle_message routine in mod.rs - I'd be happy to try to put together a pull request if that function is the right place to put this logic, since most of the work is already done and just needs to be 'transposed' into the correct place.

If someone more knowledgable about this library and IRC in general could give me some guidance or a push in the right direction on this it would be much appreciated! Or maybe I'm missing something obvious and I'm massively overcomplicating this whole thing.

Thanks in advance, Savoir

savoiringfaire commented 3 years ago

Ah, I see the ping timeouts may be related to the last comment in #214 which seems to have come to the same conclusion I did regarding waiting until after the MOTD has been sent (I had written that thread off since it was talking about default timeout times which didn't look like my issue, and didn't read the last message!). I think the rest of this is still applicable regarding the most idiomatic way of handling SASL basic auth, however. There is also #207, however the fix of increasing the ping timeout didn't work for me - even up to 60 seconds. I assume this is because the ping is sent so early in the process that the freenode servers ignore it and aren't just slow to send back a ping but, in fact, never send back a ping - the specific settings I tried were:

ping_time=180
ping_timeout=60
aatxe commented 3 years ago

Sure, I would be willing to accept (separate) pull requests for both a SASL plain auth method (if I recall correctly, it's not there because someone indicated to me at some point that it was not used by any servers, but evidently that's not the case) and for an option for ignoring non-ping responses before MOTD (cc @erikh).

kushaldas commented 2 years ago

@aatxe @savoiringfaire Can we please have this SASL mechanism as an example or part of the documentation?