// Process a single connection. func process(cid int, nc net.Conn, certs []tls.Certificate, logf io.Writer, smtplog io.Writer, baserules []*Rule) { var evt smtpd.EventInfo var convo *smtpd.Conn var logger *smtpLogger var l2 io.Writer var gotsomewhere, stall, sesscounts bool var cfg smtpd.Config defer nc.Close() trans := &smtpTransaction{} trans.savedir = savedir trans.raddr = nc.RemoteAddr() trans.laddr = nc.LocalAddr() prefix := fmt.Sprintf("%d/%d", os.Getpid(), cid) trans.rip, _, _ = net.SplitHostPort(trans.raddr.String()) trans.lip, _, _ = net.SplitHostPort(trans.laddr.String()) var c *Context // nit: in the presence of yakkers, we must know whether or not // the rules are good because bad rules turn *everyone* into // yakkers (since they prevent clients from successfully EHLO'ing). rules, rulesgood := setupRules(baserules) // A yakker is a client that is repeatedly connecting to us // without doing anything successfully. After a certain number // of attempts we turn them off. We only do this if we're logging // SMTP commands; if we're not logging, we don't care. // This is kind of a hack, but this code is for Chris and this is // what Chris cares about. // sesscounts is true if this session should count for being a // 'bad' session if we don't get far enough. Sessions with TLS // errors don't count, as do sessions with bad rules or sessions // where yakCount == 0. sesscounts = rulesgood && yakCount > 0 hit, cnt := yakkers.Lookup(trans.rip, yakTimeout) if yakCount > 0 && hit && cnt >= yakCount && smtplog != nil { // nit: if the rules are bad and we're stalling anyways, // yakkers still have their SMTP transactions not logged. c = newContext(trans, stallall) stall = true sesscounts = false } else { c = newContext(trans, rules) } //fmt.Printf("rules are:\n%+v\n", c.ruleset) if smtplog != nil && !stall { logger = &smtpLogger{} logger.prefix = []byte(prefix) logger.writer = bufio.NewWriterSize(smtplog, 8*1024) trans.log = logger l2 = logger } sname := trans.laddr.String() if srvname != "" { sname = srvname } else { lip, _, _ := net.SplitHostPort(sname) // we don't do a verified lookup of the local IP address // because it's theoretically under your control, so if // you want to forge stuff that's up to you. nlst, err := net.LookupAddr(lip) if err == nil && len(nlst) > 0 { sname = nlst[0] if sname[len(sname)-1] == '.' { sname = sname[:len(sname)-1] } } } if connfile != "" { dm, err := loadConnFile(connfile) if err != nil { warnf("error loading per-connection rules '%s': %s\n", connfile, err) } // dm.find() explicitly works even on nil dm, so we don't // need to guard it. if pd := dm.find(nc); pd != nil { if pd.myname != "" { sname = pd.myname } certs = pd.certs } } cfg.LocalName = sname cfg.SayTime = true cfg.SftName = "sinksmtp" cfg.Announce = "This server does not deliver email." // stalled conversations are always slow, even if -S is not set. // TODO: make them even slower than this? I probably don't care. if goslow || stall { cfg.Delay = time.Second / 10 } // Don't offer TLS to hosts that have too many TLS failures. // We give hosts *two* tries at setting up TLS because some // hosts start by offering SSLv2, which is an instant-fail, // even if they support stuff that we do. We hope that their // SSLv2 failure will cause them to try again in another // connection with TLS only. // See https://code.google.com/p/go/issues/detail?id=3930 blocktls, blcount := notls.Lookup(trans.rip, tlsTimeout) if len(certs) > 0 && !(blocktls && blcount >= 2) { var tlsc tls.Config tlsc.Certificates = certs // if there is already one TLS failure for this host, // it might be because of a bad client certificate. // so on the second time around we don't ask for one. // (More precisely we only ask for a client cert if // there are no failures so far.) // Another reason for failure here is a SSLv3 only // host without a client certificate. This produces // the error: // tls: received unexpected handshake message of type *tls.clientKeyExchangeMsg when waiting for *tls.certificateMsg //if blcount == 0 { // tlsc.ClientAuth = tls.VerifyClientCertIfGiven //} // Now generally disabled since I discovered it causes // SSLv3 handshakes to always fail. TODO: better fix with // config-file control or something. tlsc.SessionTicketsDisabled = true tlsc.ServerName = sname tlsc.BuildNameToCertificate() cfg.TLSConfig = &tlsc } // With everything set up we can now create the connection. convo = smtpd.NewConn(nc, cfg, l2) // Yes, we do rDNS lookup before our initial greeting banner and // thus can pause a bit here. Clients will cope, or at least we // don't care if impatient ones don't. trans.rdns, _ = LookupAddrVerified(trans.rip) // Check for an immediate result on the initial connection. This // may disable TLS or refuse things immediately. if decider(pConnect, evt, c, convo, "") { // TODO: somehow write a message and maybe log it. // this probably needs smtpd.go cooperation. // Right now we just close abruptly. if !stall { writeLog(logger, "! %s dropped on connect due to rule at %s\n", trans.rip, time.Now().Format(smtpd.TimeFmt)) } return } // Main transaction loop. We gather up email messages as they come // in, possibly failing various operations as we're told to. for { evt = convo.Next() switch evt.What { case smtpd.COMMAND: switch evt.Cmd { case smtpd.EHLO, smtpd.HELO: if decider(pHelo, evt, c, convo, "") { continue } trans.heloname = evt.Arg trans.from = "" trans.data = "" trans.hash = "" trans.bodyhash = "" trans.rcptto = []string{} if minphase == "helo" { gotsomewhere = true } case smtpd.MAILFROM: if decider(pMfrom, evt, c, convo, "") { continue } if trans.from != "" && !gotsomewhere && sesscounts { // We've been RSET, which potentially // counts as a failure for do-nothing // client detection. Note that we are // implicitly adding the *last* failed // attempt, the one that was RSET from. cnt = yakkers.Add(trans.rip, yakTimeout) // We're slightly generous with RSETs. // This has no net effect unless this // final attempt succeeds. if cnt > yakCount { writeLog(logger, "! %s added as a yakker at hit %d due to RSET\n", trans.rip, cnt) convo.TempfailMsg("Too many unsuccessful delivery attempts") // this will implicitly close // the connection. return } } trans.from = evt.Arg trans.data = "" trans.rcptto = []string{} if minphase == "from" { gotsomewhere = true } doAccept(convo, c, "") case smtpd.RCPTTO: if decider(pRto, evt, c, convo, "") { continue } trans.rcptto = append(trans.rcptto, evt.Arg) if minphase == "to" { gotsomewhere = true } doAccept(convo, c, "") case smtpd.DATA: if decider(pData, evt, c, convo, "") { continue } if minphase == "data" { gotsomewhere = true } doAccept(convo, c, "") } case smtpd.GOTDATA: // -minphase=message means 'message // successfully transmitted to us' as opposed // to 'message accepted'. if minphase == "message" { gotsomewhere = true } // message rejection is deferred until after logging // et al. trans.data = evt.Arg trans.when = time.Now() trans.tlson = convo.TLSOn trans.cipher = convo.TLSState.CipherSuite trans.servername = convo.TLSState.ServerName trans.tlsversion = convo.TLSState.Version trans.hash, trans.bodyhash = getHashes(trans) transid, err := handleMessage(prefix, trans, logf) // errors when handling a message always force // a tempfail regardless of how we're // configured. switch { case err != nil: convo.Tempfail() gotsomewhere = true case decider(pMessage, evt, c, convo, transid): // do nothing, already handled default: if minphase == "accepted" { gotsomewhere = true } doAccept(convo, c, transid) } case smtpd.TLSERROR: // any TLS error means we'll avoid offering TLS // to this source IP for a while. notls.Add(trans.rip, tlsTimeout) sesscounts = false } if evt.What == smtpd.DONE || evt.What == smtpd.ABORT { break } } // if the client did not issue any successful meaningful commands, // remember this. we squelch people who yak too long. // Once people are yakkers we don't count their continued failure // to do anything against them. // And we have to have good rules to start with because duh. switch { case !gotsomewhere && sesscounts: cnt = yakkers.Add(trans.rip, yakTimeout) // See if this transaction has pushed the client over the // edge to becoming a yakker. If so, report it to the SMTP // log. if cnt >= yakCount { writeLog(logger, "! %s added as a yakker at hit %d\n", trans.rip, cnt) } case yakCount > 0 && gotsomewhere: yakkers.Del(trans.rip) } }
// Decide what to do and then do it if it is a rejection or a tempfail. // If given an id (and it is in the message handling phase) we call // RejectData(). This is our convenience driver for the rules engine, // Decide(). // // Returns false if the message was accepted, true if decider() handled // a rejection or tempfail. func decider(ph Phase, evt smtpd.EventInfo, c *Context, convo *smtpd.Conn, id string) bool { res := Decide(ph, evt, c) logDnsbls(c) // Terrible hack to log DNS lookup failure specifics. if c.domerr != nil && c.trans.log != nil { lmsg := fmt.Sprintf("! %s\n", c.domerr) if lmsg != c.trans.lastamsg { c.trans.log.Write([]byte(lmsg)) c.trans.lastamsg = lmsg } } // The moment a rule sets a savedir, it becomes sticky. // This lets you select a savedir based on eg from matching // instead of having to do games later. if sd := c.withprops["savedir"]; sd != "" { c.trans.savedir = sd } // rule notes are deliberately logged every time they hit. // this may be a mistake given EHLO retrying as HELO, but // I'll see. if note := c.withprops["note"]; note != "" { c.trans.log.Write([]byte(fmt.Sprintf("! rule note: %s\n", note))) } // Disable TLS if desired, or just disable asking for client certs. switch c.withprops["tls-opt"] { case "off": convo.Config.TLSConfig = nil case "no-client": // 'tls-opt no-client' without certificates should not // crash. if convo.Config.TLSConfig != nil { convo.Config.TLSConfig.ClientAuth = tls.NoClientCert } } if res == aNoresult || res == aAccept { return false } if ph == pConnect { // TODO: have some way to stall or reject connections // in smtpd. Or should that be handled outside of it? // Right now a reject result means 'drop', stall will // implicitly cause us to go on. return res == aReject } msg := c.withprops["message"] switch res { case aReject: // This is kind of a hack. // We assume that 'id' is only set when we should report it, // which is kind of safe. if msg != "" { if id != "" { msg += "\nRejected with ID " + id } convo.RejectMsg(msg) return true } // Default messages are kind of intricate. switch { case id != "" && ph == pMessage: convo.RejectMsg("We do not consent to you emailing %s\nRejected with ID %s", pluralRecips(c), id) case ph == pMessage || ph == pData: convo.RejectMsg("We do not consent to you emailing %s", pluralRecips(c)) case ph == pRto: convo.RejectMsg("We do not consent to you emailing that address") default: convo.Reject() } case aStall: if msg != "" { convo.TempfailMsg(msg) } else { convo.Tempfail() } default: panic("impossible res") } return true }