Index: trunk/bridge.go
===================================================================
--- trunk/bridge.go	(revision 138)
+++ trunk/bridge.go	(revision 139)
@@ -30,8 +30,5 @@
 	// TODO: send multiple members in each message
 	for nick, membership := range ch.Members {
-		s := dc.marshalNick(ch.conn, nick)
-		if membership != 0 {
-			s = string(membership) + s
-		}
+		s := membership.String() + dc.marshalNick(ch.conn, nick)
 
 		dc.SendMessage(&irc.Message{
Index: trunk/downstream.go
===================================================================
--- trunk/downstream.go	(revision 138)
+++ trunk/downstream.go	(revision 139)
@@ -852,39 +852,5 @@
 		}
 
-		uc, upstreamName, err := dc.unmarshalEntity(name)
-		if err != nil {
-			return err
-		}
-
-		if uc.isChannel(upstreamName) {
-			// TODO: handle MODE channel mode arguments
-			if modeStr != "" {
-				uc.SendMessage(&irc.Message{
-					Command: "MODE",
-					Params:  []string{upstreamName, modeStr},
-				})
-			} else {
-				ch, ok := uc.channels[upstreamName]
-				if !ok {
-					return ircError{&irc.Message{
-						Command: irc.ERR_NOSUCHCHANNEL,
-						Params:  []string{dc.nick, name, "No such channel"},
-					}}
-				}
-
-				dc.SendMessage(&irc.Message{
-					Prefix:  dc.srv.prefix(),
-					Command: irc.RPL_CHANNELMODEIS,
-					Params:  []string{dc.nick, name, string(ch.modes)},
-				})
-			}
-		} else {
-			if name != dc.nick {
-				return ircError{&irc.Message{
-					Command: irc.ERR_USERSDONTMATCH,
-					Params:  []string{dc.nick, "Cannot change mode for other users"},
-				}}
-			}
-
+		if name == dc.nick {
 			if modeStr != "" {
 				dc.forEachUpstream(func(uc *upstreamConn) {
@@ -901,4 +867,50 @@
 				})
 			}
+			return nil
+		}
+
+		uc, upstreamName, err := dc.unmarshalEntity(name)
+		if err != nil {
+			return err
+		}
+
+		if !uc.isChannel(upstreamName) {
+			return ircError{&irc.Message{
+				Command: irc.ERR_USERSDONTMATCH,
+				Params:  []string{dc.nick, "Cannot change mode for other users"},
+			}}
+		}
+
+		if modeStr != "" {
+			params := []string{upstreamName, modeStr}
+			params = append(params, msg.Params[2:]...)
+			uc.SendMessage(&irc.Message{
+				Command: "MODE",
+				Params:  params,
+			})
+		} else {
+			ch, ok := uc.channels[upstreamName]
+			if !ok {
+				return ircError{&irc.Message{
+					Command: irc.ERR_NOSUCHCHANNEL,
+					Params:  []string{dc.nick, name, "No such channel"},
+				}}
+			}
+
+			if ch.modes == nil {
+				// we haven't received the initial RPL_CHANNELMODEIS yet
+				// ignore the request, we will broadcast the modes later when we receive RPL_CHANNELMODEIS
+				return nil
+			}
+
+			modeStr, modeParams := ch.modes.Format()
+			params := []string{dc.nick, name, modeStr}
+			params = append(params, modeParams...)
+
+			dc.SendMessage(&irc.Message{
+				Prefix:  dc.srv.prefix(),
+				Command: irc.RPL_CHANNELMODEIS,
+				Params:  params,
+			})
 		}
 	case "WHO":
Index: trunk/irc.go
===================================================================
--- trunk/irc.go	(revision 138)
+++ trunk/irc.go	(revision 139)
@@ -16,17 +16,17 @@
 )
 
-type modeSet string
+type userModes string
 
-func (ms modeSet) Has(c byte) bool {
+func (ms userModes) Has(c byte) bool {
 	return strings.IndexByte(string(ms), c) >= 0
 }
 
-func (ms *modeSet) Add(c byte) {
+func (ms *userModes) Add(c byte) {
 	if !ms.Has(c) {
-		*ms += modeSet(c)
+		*ms += userModes(c)
 	}
 }
 
-func (ms *modeSet) Del(c byte) {
+func (ms *userModes) Del(c byte) {
 	i := strings.IndexByte(string(*ms), c)
 	if i >= 0 {
@@ -35,5 +35,5 @@
 }
 
-func (ms *modeSet) Apply(s string) error {
+func (ms *userModes) Apply(s string) error {
 	var plusMinus byte
 	for i := 0; i < len(s); i++ {
@@ -54,4 +54,92 @@
 	return nil
 }
+
+type channelModeType byte
+
+// standard channel mode types, as explained in https://modern.ircdocs.horse/#mode-message
+const (
+	// modes that add or remove an address to or from a list
+	modeTypeA channelModeType = iota
+	// modes that change a setting on a channel, and must always have a parameter
+	modeTypeB
+	// modes that change a setting on a channel, and must have a parameter when being set, and no parameter when being unset
+	modeTypeC
+	// modes that change a setting on a channel, and must not have a parameter
+	modeTypeD
+)
+
+var stdChannelModes = map[byte]channelModeType{
+	'b': modeTypeA, // ban list
+	'e': modeTypeA, // ban exception list
+	'I': modeTypeA, // invite exception list
+	'k': modeTypeB, // channel key
+	'l': modeTypeC, // channel user limit
+	'i': modeTypeD, // channel is invite-only
+	'm': modeTypeD, // channel is moderated
+	'n': modeTypeD, // channel has no external messages
+	's': modeTypeD, // channel is secret
+	't': modeTypeD, // channel has protected topic
+}
+
+type channelModes map[byte]string
+
+func (cm channelModes) Apply(modeTypes map[byte]channelModeType, modeStr string, arguments ...string) error {
+	nextArgument := 0
+	var plusMinus byte
+	for i := 0; i < len(modeStr); i++ {
+		mode := modeStr[i]
+		if mode == '+' || mode == '-' {
+			plusMinus = mode
+			continue
+		}
+		if plusMinus != '+' && plusMinus != '-' {
+			return fmt.Errorf("malformed modestring %q: missing plus/minus", modeStr)
+		}
+
+		mt, ok := modeTypes[mode]
+		if !ok {
+			continue
+		}
+		if mt == modeTypeB || (mt == modeTypeC && plusMinus == '+') {
+			if plusMinus == '+' {
+				var argument string
+				// some sentitive arguments (such as channel keys) can be omitted for privacy
+				// (this will only happen for RPL_CHANNELMODEIS, never for MODE messages)
+				if nextArgument < len(arguments) {
+					argument = arguments[nextArgument]
+				}
+				cm[mode] = argument
+			} else {
+				delete(cm, mode)
+			}
+			nextArgument++
+		} else if mt == modeTypeC || mt == modeTypeD {
+			if plusMinus == '+' {
+				cm[mode] = ""
+			} else {
+				delete(cm, mode)
+			}
+		}
+	}
+	return nil
+}
+
+func (cm channelModes) Format() (modeString string, parameters []string) {
+	var modesWithValues strings.Builder
+	var modesWithoutValues strings.Builder
+	parameters = make([]string, 0, 16)
+	for mode, value := range cm {
+		if value != "" {
+			modesWithValues.WriteString(string(mode))
+			parameters = append(parameters, value)
+		} else {
+			modesWithoutValues.WriteString(string(mode))
+		}
+	}
+	modeString = "+" + modesWithValues.String() + modesWithoutValues.String()
+	return
+}
+
+const stdChannelTypes = "#&+!"
 
 type channelStatus byte
@@ -75,30 +163,22 @@
 }
 
-type membership byte
+type membership struct {
+	Mode   byte
+	Prefix byte
+}
 
-const (
-	membershipFounder   membership = '~'
-	membershipProtected membership = '&'
-	membershipOperator  membership = '@'
-	membershipHalfOp    membership = '%'
-	membershipVoice     membership = '+'
-)
+var stdMemberships = []membership{
+	{'q', '~'}, // founder
+	{'a', '&'}, // protected
+	{'o', '@'}, // operator
+	{'h', '%'}, // halfop
+	{'v', '+'}, // voice
+}
 
-const stdMembershipPrefixes = "~&@%+"
-
-func (m membership) String() string {
-	if m == 0 {
+func (m *membership) String() string {
+	if m == nil {
 		return ""
 	}
-	return string(m)
-}
-
-func parseMembershipPrefix(s string) (prefix membership, nick string) {
-	// TODO: any prefix from PREFIX RPL_ISUPPORT
-	if strings.IndexByte(stdMembershipPrefixes, s[0]) >= 0 {
-		return membership(s[0]), s[1:]
-	} else {
-		return 0, s
-	}
+	return string(m.Prefix)
 }
 
Index: trunk/upstream.go
===================================================================
--- trunk/upstream.go	(revision 138)
+++ trunk/upstream.go	(revision 139)
@@ -22,6 +22,6 @@
 	TopicTime time.Time
 	Status    channelStatus
-	modes     modeSet
-	Members   map[string]membership
+	modes     channelModes
+	Members   map[string]*membership
 	complete  bool
 }
@@ -39,6 +39,7 @@
 	serverName            string
 	availableUserModes    string
-	availableChannelModes string
-	channelModesWithParam string
+	availableChannelModes map[byte]channelModeType
+	availableChannelTypes string
+	availableMemberships  []membership
 
 	registered bool
@@ -47,5 +48,5 @@
 	realname   string
 	closed     bool
-	modes      modeSet
+	modes      userModes
 	channels   map[string]*upstreamChannel
 	caps       map[string]string
@@ -73,14 +74,17 @@
 	outgoing := make(chan *irc.Message, 64)
 	uc := &upstreamConn{
-		network:  network,
-		logger:   logger,
-		net:      netConn,
-		irc:      irc.NewConn(netConn),
-		srv:      network.user.srv,
-		user:     network.user,
-		outgoing: outgoing,
-		ring:     NewRing(network.user.srv.RingCap),
-		channels: make(map[string]*upstreamChannel),
-		caps:     make(map[string]string),
+		network:               network,
+		logger:                logger,
+		net:                   netConn,
+		irc:                   irc.NewConn(netConn),
+		srv:                   network.user.srv,
+		user:                  network.user,
+		outgoing:              outgoing,
+		ring:                  NewRing(network.user.srv.RingCap),
+		channels:              make(map[string]*upstreamChannel),
+		caps:                  make(map[string]string),
+		availableChannelTypes: stdChannelTypes,
+		availableChannelModes: stdChannelModes,
+		availableMemberships:  stdMemberships,
 	}
 
@@ -131,13 +135,17 @@
 
 func (uc *upstreamConn) isChannel(entity string) bool {
-	for _, r := range entity {
-		switch r {
-		// TODO: support upstream ISUPPORT channel prefixes
-		case '#', '&', '+', '!':
-			return true
-		}
-		break
+	if i := strings.IndexByte(uc.availableChannelTypes, entity[0]); i >= 0 {
+		return true
 	}
 	return false
+}
+
+func (uc *upstreamConn) parseMembershipPrefix(s string) (membership *membership, nick string) {
+	for _, m := range uc.availableMemberships {
+		if m.Prefix == s[0] {
+			return &m, s[1:]
+		}
+	}
+	return nil, s
 }
 
@@ -150,33 +158,4 @@
 		})
 		return nil
-	case "MODE":
-		var name, modeStr string
-		if err := parseMessageParams(msg, &name, &modeStr); err != nil {
-			return err
-		}
-
-		if !uc.isChannel(name) { // user mode change
-			if name != uc.nick {
-				return fmt.Errorf("received MODE message for unknown nick %q", name)
-			}
-			return uc.modes.Apply(modeStr)
-		} else { // channel mode change
-			// TODO: handle MODE channel mode arguments
-			ch, err := uc.getChannel(name)
-			if err != nil {
-				return err
-			}
-			if err := ch.modes.Apply(modeStr); err != nil {
-				return err
-			}
-
-			uc.forEachDownstream(func(dc *downstreamConn) {
-				dc.SendMessage(&irc.Message{
-					Prefix:  dc.marshalUserPrefix(uc, msg.Prefix),
-					Command: "MODE",
-					Params:  []string{dc.marshalChannel(uc, name), modeStr},
-				})
-			})
-		}
 	case "NOTICE":
 		uc.logger.Print(msg)
@@ -347,9 +326,65 @@
 		}
 	case irc.RPL_MYINFO:
-		if err := parseMessageParams(msg, nil, &uc.serverName, nil, &uc.availableUserModes, &uc.availableChannelModes); err != nil {
-			return err
-		}
-		if len(msg.Params) > 5 {
-			uc.channelModesWithParam = msg.Params[5]
+		if err := parseMessageParams(msg, nil, &uc.serverName, nil, &uc.availableUserModes, nil); err != nil {
+			return err
+		}
+	case irc.RPL_ISUPPORT:
+		if err := parseMessageParams(msg, nil, nil); err != nil {
+			return err
+		}
+		for _, token := range msg.Params[1 : len(msg.Params)-1] {
+			negate := false
+			parameter := token
+			value := ""
+			if strings.HasPrefix(token, "-") {
+				negate = true
+				token = token[1:]
+			} else {
+				if i := strings.IndexByte(token, '='); i >= 0 {
+					parameter = token[:i]
+					value = token[i+1:]
+				}
+			}
+			if !negate {
+				switch parameter {
+				case "CHANMODES":
+					parts := strings.SplitN(value, ",", 5)
+					if len(parts) < 4 {
+						return fmt.Errorf("malformed ISUPPORT CHANMODES value: %v", value)
+					}
+					modes := make(map[byte]channelModeType)
+					for i, mt := range []channelModeType{modeTypeA, modeTypeB, modeTypeC, modeTypeD} {
+						for j := 0; j < len(parts[i]); j++ {
+							mode := parts[i][j]
+							modes[mode] = mt
+						}
+					}
+					uc.availableChannelModes = modes
+				case "CHANTYPES":
+					uc.availableChannelTypes = value
+				case "PREFIX":
+					if value == "" {
+						uc.availableMemberships = nil
+					} else {
+						if value[0] != '(' {
+							return fmt.Errorf("malformed ISUPPORT PREFIX value: %v", value)
+						}
+						sep := strings.IndexByte(value, ')')
+						if sep < 0 || len(value) != sep*2 {
+							return fmt.Errorf("malformed ISUPPORT PREFIX value: %v", value)
+						}
+						memberships := make([]membership, len(value)/2-1)
+						for i := range memberships {
+							memberships[i] = membership{
+								Mode:   value[i+1],
+								Prefix: value[sep+i+1],
+							}
+						}
+						uc.availableMemberships = memberships
+					}
+				}
+			} else {
+				// TODO: handle ISUPPORT negations
+			}
 		}
 	case "NICK":
@@ -400,6 +435,11 @@
 					Name:    ch,
 					conn:    uc,
-					Members: make(map[string]membership),
+					Members: make(map[string]*membership),
 				}
+
+				uc.SendMessage(&irc.Message{
+					Command: "MODE",
+					Params:  []string{ch},
+				})
 			} else {
 				ch, err := uc.getChannel(ch)
@@ -407,5 +447,5 @@
 					return err
 				}
-				ch.Members[msg.Prefix.Name] = 0
+				ch.Members[msg.Prefix.Name] = nil
 			}
 
@@ -509,4 +549,87 @@
 			})
 		})
+	case "MODE":
+		var name, modeStr string
+		if err := parseMessageParams(msg, &name, &modeStr); err != nil {
+			return err
+		}
+
+		if !uc.isChannel(name) { // user mode change
+			if name != uc.nick {
+				return fmt.Errorf("received MODE message for unknown nick %q", name)
+			}
+			return uc.modes.Apply(modeStr)
+			// TODO: notify downstreams about user mode change?
+		} else { // channel mode change
+			ch, err := uc.getChannel(name)
+			if err != nil {
+				return err
+			}
+
+			if ch.modes != nil {
+				if err := ch.modes.Apply(uc.availableChannelModes, modeStr, msg.Params[2:]...); err != nil {
+					return err
+				}
+			}
+
+			uc.forEachDownstream(func(dc *downstreamConn) {
+				params := []string{dc.marshalChannel(uc, name), modeStr}
+				params = append(params, msg.Params[2:]...)
+
+				dc.SendMessage(&irc.Message{
+					Prefix:  dc.marshalUserPrefix(uc, msg.Prefix),
+					Command: "MODE",
+					Params:  params,
+				})
+			})
+		}
+	case irc.RPL_UMODEIS:
+		if err := parseMessageParams(msg, nil); err != nil {
+			return err
+		}
+		modeStr := ""
+		if len(msg.Params) > 1 {
+			modeStr = msg.Params[1]
+		}
+
+		uc.modes = ""
+		if err := uc.modes.Apply(modeStr); err != nil {
+			return err
+		}
+		// TODO: send RPL_UMODEIS to downstream connections when applicable
+	case irc.RPL_CHANNELMODEIS:
+		var channel string
+		if err := parseMessageParams(msg, nil, &channel); err != nil {
+			return err
+		}
+		modeStr := ""
+		if len(msg.Params) > 2 {
+			modeStr = msg.Params[2]
+		}
+
+		ch, err := uc.getChannel(channel)
+		if err != nil {
+			return err
+		}
+
+		firstMode := ch.modes == nil
+		ch.modes = make(map[byte]string)
+		if err := ch.modes.Apply(uc.availableChannelModes, modeStr, msg.Params[3:]...); err != nil {
+			return err
+		}
+		if firstMode {
+			modeStr, modeParams := ch.modes.Format()
+
+			uc.forEachDownstream(func(dc *downstreamConn) {
+				params := []string{dc.nick, dc.marshalChannel(uc, channel), modeStr}
+				params = append(params, modeParams...)
+
+				dc.SendMessage(&irc.Message{
+					Prefix:  dc.srv.prefix(),
+					Command: irc.RPL_CHANNELMODEIS,
+					Params:  params,
+				})
+			})
+		}
 	case rpl_topicwhotime:
 		var name, who, timeStr string
@@ -541,5 +664,5 @@
 
 		for _, s := range strings.Split(members, " ") {
-			membership, nick := parseMembershipPrefix(s)
+			membership, nick := uc.parseMembershipPrefix(s)
 			ch.Members[nick] = membership
 		}
@@ -680,5 +803,5 @@
 			channelList := make([]string, len(channels))
 			for i, channel := range channels {
-				prefix, channel := parseMembershipPrefix(channel)
+				prefix, channel := uc.parseMembershipPrefix(channel)
 				channel = dc.marshalChannel(uc, channel)
 				channelList[i] = prefix.String() + channel
