예제 #1
0
func doAccept(convo *smtpd.Conn, c *Context, transid string) {
	msg := c.withprops["message"]
	switch {
	case msg != "":
		if transid != "" {
			msg += "\nAccepted with ID " + transid
		}
		convo.AcceptMsg(msg)
	case transid != "":
		convo.AcceptData(transid)
	default:
		convo.Accept()
	}
}
예제 #2
0
// 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)
	}
}
예제 #3
0
// 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
}