diff --git a/cmd/soju/main.go b/cmd/soju/main.go index ae193f7..a3727d2 100644 --- a/cmd/soju/main.go +++ b/cmd/soju/main.go @@ -93,6 +93,7 @@ func loadConfig() (*config.Server, *soju.Config, error) { AcceptProxyIPs: raw.AcceptProxyIPs, MaxUserNetworks: raw.MaxUserNetworks, MultiUpstream: raw.MultiUpstream, + UpstreamUserIPs: raw.UpstreamUserIPs, Debug: debug, MOTD: motd, } diff --git a/config/config.go b/config/config.go index 410d9c2..07b7124 100644 --- a/config/config.go +++ b/config/config.go @@ -52,6 +52,7 @@ type Server struct { MaxUserNetworks int MultiUpstream bool + UpstreamUserIPs []*net.IPNet } func Defaults() *Server { @@ -150,6 +151,29 @@ func parse(cfg scfg.Block) (*Server, error) { return nil, fmt.Errorf("directive %q: %v", d.Name, err) } srv.MultiUpstream = v + case "upstream-user-ip": + if len(srv.UpstreamUserIPs) > 0 { + return nil, fmt.Errorf("directive %q: can only be specified once", d.Name) + } + var hasIPv4, hasIPv6 bool + for _, s := range d.Params { + _, n, err := net.ParseCIDR(s) + if err != nil { + return nil, fmt.Errorf("directive %q: failed to parse CIDR: %v", d.Name, err) + } + if n.IP.To4() == nil { + if hasIPv6 { + return nil, fmt.Errorf("directive %q: found two IPv6 CIDRs", d.Name) + } + hasIPv6 = true + } else { + if hasIPv4 { + return nil, fmt.Errorf("directive %q: found two IPv4 CIDRs", d.Name) + } + hasIPv4 = true + } + srv.UpstreamUserIPs = append(srv.UpstreamUserIPs, n) + } default: return nil, fmt.Errorf("unknown directive %q", d.Name) } diff --git a/doc/soju.1.scd b/doc/soju.1.scd index 12442b1..3ec3fb0 100644 --- a/doc/soju.1.scd +++ b/doc/soju.1.scd @@ -156,6 +156,15 @@ The following directives are supported: Globally enable or disable multi-upstream mode. By default, multi-upstream mode is enabled. +*upstream-user-ip* + Enable per-user IP addresses. One IPv4 range and/or one IPv6 range can be + specified in CIDR notation. One IP address per range will be assigned to + each user and will be used as the source address when connecting to an + upstream network. + + This can be useful to avoid having the whole bouncer banned from an upstream + network because of one malicious user. + # IRC SERVICE soju exposes an IRC service called *BouncerServ* to manage the bouncer. diff --git a/server.go b/server.go index b125def..d844b27 100644 --- a/server.go +++ b/server.go @@ -64,6 +64,7 @@ type Config struct { MaxUserNetworks int MultiUpstream bool MOTD string + UpstreamUserIPs []*net.IPNet } type Server struct { diff --git a/upstream.go b/upstream.go index 201213a..077d792 100644 --- a/upstream.go +++ b/upstream.go @@ -140,6 +140,11 @@ func connectToUpstream(network *network) (*upstreamConn, error) { addr = u.Host + ":6697" } + dialer.LocalAddr, err = network.user.localTCPAddrForHost(host) + if err != nil { + return nil, fmt.Errorf("failed to pick local IP for remote host %q: %v", host, err) + } + logger.Printf("connecting to TLS server at address %q", addr) tlsConfig := &tls.Config{ServerName: host, NextProtos: []string{"irc"}} @@ -174,8 +179,15 @@ func connectToUpstream(network *network) (*upstreamConn, error) { netConn = tls.Client(netConn, tlsConfig) case "irc+insecure": addr := u.Host - if _, _, err := net.SplitHostPort(addr); err != nil { - addr = addr + ":6667" + host, _, err := net.SplitHostPort(addr) + if err != nil { + host = u.Host + addr = u.Host + ":6667" + } + + dialer.LocalAddr, err = network.user.localTCPAddrForHost(host) + if err != nil { + return nil, fmt.Errorf("failed to pick local IP for remote host %q: %v", host, err) } logger.Printf("connecting to plain-text server at address %q", addr) diff --git a/user.go b/user.go index b573ff1..45bb806 100644 --- a/user.go +++ b/user.go @@ -6,6 +6,8 @@ import ( "encoding/binary" "encoding/hex" "fmt" + "math/big" + "net" "time" "gopkg.in/irc.v3" @@ -956,3 +958,46 @@ func (u *user) hasPersistentMsgStore() bool { _, isMem := u.msgStore.(*memoryMessageStore) return !isMem } + +// localAddrForHost returns the local address to use when connecting to host. +// A nil address is returned when the OS should automatically pick one. +func (u *user) localTCPAddrForHost(host string) (*net.TCPAddr, error) { + upstreamUserIPs := u.srv.Config().UpstreamUserIPs + if len(upstreamUserIPs) == 0 { + return nil, nil + } + + ips, err := net.LookupIP(host) + if err != nil { + return nil, err + } + + wantIPv6 := false + for _, ip := range ips { + if ip.To4() == nil { + wantIPv6 = true + break + } + } + + var ipNet *net.IPNet + for _, in := range upstreamUserIPs { + if wantIPv6 == (in.IP.To4() == nil) { + ipNet = in + break + } + } + if ipNet == nil { + return nil, nil + } + + var ipInt big.Int + ipInt.SetBytes(ipNet.IP) + ipInt.Add(&ipInt, big.NewInt(u.ID+1)) + ip := net.IP(ipInt.Bytes()) + if !ipNet.Contains(ip) { + return nil, fmt.Errorf("IP network %v too small", ipNet) + } + + return &net.TCPAddr{IP: ip}, nil +}