diff --git a/interface-ip.c b/interface-ip.c index 7e60f64..ef6e907 100644 --- a/interface-ip.c +++ b/interface-ip.c @@ -30,6 +30,8 @@ #include "ubus.h" #include "system.h" +#define MAX_SEARCH_DOMAINS 50 /* Search domain maximum number that can be stored under /etc/resolv.conf */ + enum { ROUTE_INTERFACE, ROUTE_TARGET, @@ -90,6 +92,27 @@ const struct uci_blob_param_list neighbor_attr_list = { .params = neighbor_attr, }; +enum { + DNS_HOST, + DNS_LIFETIME, + __DNS_MAX +}; + +static const struct blobmsg_policy dns_attr[__DNS_MAX]= { + [DNS_HOST]= { .name = "dns", .type = BLOBMSG_TYPE_STRING}, + [DNS_LIFETIME]= { .name = "lifetime", .type = BLOBMSG_TYPE_INT32}, +}; + +enum { + DNS_SEARCH_DOMAIN, + DNS_SEARCH_LIFETIME, + __DNS_SEARCH_MAX +}; + +static const struct blobmsg_policy dns_search_attr[__DNS_SEARCH_MAX]= { + [DNS_SEARCH_DOMAIN]= { .name = "domain", .type = BLOBMSG_TYPE_STRING}, + [DNS_SEARCH_LIFETIME]= { .name = "lifetime", .type = BLOBMSG_TYPE_INT32}, +}; struct list_head prefixes = LIST_HEAD_INIT(prefixes); static struct device_prefix *ula_prefix = NULL; @@ -714,6 +737,9 @@ interface_update_proto_addr(struct vlist_tree *tree, interface_handle_subnet_route(iface, a_old, false); system_del_address(dev, a_old); + if ((a_old->flags & DEVADDR_FAMILY) == DEVADDR_INET6) + v6 = true; + if ((a_old->flags & DEVADDR_OFFLINK) && (a_old->mask < (v6 ? 128 : 32))) { struct device_route route; @@ -1411,86 +1437,88 @@ interface_ip_set_ula_prefix(const char *prefix) } } -static void -interface_add_dns_server(struct interface_ip_settings *ip, const char *str) +void +interface_add_dns_server(struct interface_ip_settings *ip, struct blob_attr *attr) { + struct blob_attr *tb[__DNS_MAX], *cur; struct dns_server *s; + blobmsg_parse(dns_attr, __DNS_MAX, tb, blobmsg_data(attr), blobmsg_data_len(attr)); + + cur = tb[DNS_HOST]; + if (cur == NULL) + return; + s = calloc(1, sizeof(*s)); if (!s) return; s->af = AF_INET; - if (inet_pton(s->af, str, &s->addr.in)) + if (inet_pton(s->af, (char *)blobmsg_data(cur), &s->addr.in)) goto add; s->af = AF_INET6; - if (inet_pton(s->af, str, &s->addr.in)) + if (inet_pton(s->af, (char *)blobmsg_data(cur), &s->addr.in)) goto add; free(s); return; add: - D(INTERFACE, "Add IPv%c DNS server: %s", - s->af == AF_INET6 ? '6' : '4', str); + s->valid_until = 0; + cur = tb[DNS_LIFETIME]; + if (cur != NULL) { + int64_t lifetime = blobmsg_get_u32(cur); + if (lifetime > 0) { + int64_t valid_until = lifetime + (int64_t)system_get_rtime(); + if (valid_until > 0) /* Catch overflow */ + s->valid_until = valid_until; + } + } + D(INTERFACE, "Add IPv%c DNS server: %s Expiry Time: %lld\n", + s->af == AF_INET6 ? '6' : '4', (char *)blobmsg_data(tb[DNS_HOST]), s->valid_until); vlist_simple_add(&ip->dns_servers, &s->node); } void -interface_add_dns_server_list(struct interface_ip_settings *ip, struct blob_attr *list) +interface_add_dns_search_domain(struct interface_ip_settings *ip, struct blob_attr *attr) { - struct blob_attr *cur; - size_t rem; + struct blob_attr *tb[__DNS_SEARCH_MAX], *cur; + struct dns_search_domain *s; + int len; - blobmsg_for_each_attr(cur, list, rem) { - if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING) - continue; + blobmsg_parse(dns_search_attr, __DNS_SEARCH_MAX, tb, blobmsg_data(attr), blobmsg_data_len(attr)); - if (!blobmsg_check_attr(cur, false)) - continue; - - interface_add_dns_server(ip, blobmsg_data(cur)); - } -} - -static void -interface_add_dns_search_domain(struct interface_ip_settings *ip, const char *str) -{ - struct dns_search_domain *s; - int len = strlen(str); + cur = tb[DNS_SEARCH_DOMAIN]; + if (cur == NULL) + return; + len = strlen((char *)blobmsg_data(cur)); s = calloc(1, sizeof(*s) + len + 1); if (!s) return; - D(INTERFACE, "Add DNS search domain: %s", str); - memcpy(s->name, str, len); - vlist_simple_add(&ip->dns_search, &s->node); -} - -void -interface_add_dns_search_list(struct interface_ip_settings *ip, struct blob_attr *list) -{ - struct blob_attr *cur; - size_t rem; - - blobmsg_for_each_attr(cur, list, rem) { - if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING) - continue; - - if (!blobmsg_check_attr(cur, false)) - continue; + memcpy(s->name, (char *)blobmsg_data(cur), len); + s->valid_until = 0; - interface_add_dns_search_domain(ip, blobmsg_data(cur)); + cur = tb[DNS_SEARCH_LIFETIME]; + if (cur != NULL) { + int64_t lifetime = blobmsg_get_u32(cur); + if (lifetime > 0) { + int64_t valid_until = lifetime + (int64_t)system_get_rtime(); + if (valid_until > 0) /* Catch overflow */ + s->valid_until = valid_until; + } } + + D(INTERFACE, "Add DNS search domain: %s Expiry Time: %lld\n", (char *)blobmsg_data(tb[DNS_SEARCH_DOMAIN]), s->valid_until); + vlist_simple_add(&ip->dns_search, &s->node); } static void write_resolv_conf_entries(FILE *f, struct interface_ip_settings *ip, const char *dev) { struct dns_server *s; - struct dns_search_domain *d; const char *str; char buf[INET6_ADDRSTRLEN]; @@ -1504,10 +1532,6 @@ write_resolv_conf_entries(FILE *f, struct interface_ip_settings *ip, const char else fprintf(f, "nameserver %s\n", str); } - - vlist_simple_for_each_element(&ip->dns_search, d, node) { - fprintf(f, "search %s\n", d->name); - } } /* Sorting of interface resolver entries : */ @@ -1545,9 +1569,7 @@ __interface_write_dns_entries(FILE *f, const char *jail) if (jail && (!iface->jail || strcmp(jail, iface->jail))) continue; - if (vlist_simple_empty(&iface->proto_ip.dns_search) && - vlist_simple_empty(&iface->proto_ip.dns_servers) && - vlist_simple_empty(&iface->config_ip.dns_search) && + if (vlist_simple_empty(&iface->proto_ip.dns_servers) && vlist_simple_empty(&iface->config_ip.dns_servers)) continue; @@ -1626,6 +1648,200 @@ interface_write_resolv_conf(const char *jail) } } +static void +free_search_domains(char **search_domains, int count) +{ + for (int i = 0; i < count; i++) + free(search_domains[i]); +} + +static int +gather_interface_search_domains(struct interface_ip_settings *ip, char **search_domains, int *count) +{ + struct dns_search_domain *d; + + vlist_simple_for_each_element(&ip->dns_search, d, node) { + if (*count < MAX_SEARCH_DOMAINS) { + search_domains[*count] = strdup(d->name); + if (!search_domains[*count]) + return -1; + ++(*count); + } + } + + return 0; +} + +static void +remove_search_domain_entries(FILE *origin_f, FILE *tmp_f, char **search_domains, int count) +{ + char line[256]; + + rewind(origin_f); + while (fgets(line, sizeof(line), origin_f)) { + if (!strncmp(line, "search ", 7)) { + char *token = strtok(line + 7, " \n"); + char new_line[256] = {'\0'}; + + while (token != NULL) { + bool exist = false; + + for (int i = 0; i < count; i++) { + if (!strcmp(search_domains[i], token)) { + exist = true; + break; + } + } + + if (!exist) { + // new_line cannot become longer than line , strcat can't lead to overflows + strcat(new_line, " "); + strcat(new_line, token); + } + + token = strtok(NULL, " \n"); + } + + if (new_line[0] != '\0') + fprintf(tmp_f, "search%s\n", new_line); + + } else { + fprintf(tmp_f, "%s", line); + } + } +} + +static int +write_search_domain_entries(FILE *origin_f, FILE *tmp_f, char **search_domains, int *count) +{ + char line[256]; + + rewind(origin_f); + while (fgets(line, sizeof(line), origin_f)) { + if (!strncmp(line, "search ", 7)) { + char *token = strtok(line + 7, " \n"); + + while (token != NULL) { + bool exist = false; + + for (int i = 0; i < *count; i++) { + if (!strcmp(search_domains[i], token)) { + exist = true; + break; + } + } + + if (!exist && (*count < MAX_SEARCH_DOMAINS)) { + search_domains[*count] = strdup(token); + if (!search_domains[*count]) + return -1; + ++(*count); + } + + token = strtok(NULL, " \n"); + } + } + } + + if (*count > 0) { + fprintf(tmp_f, "search"); + for (int i = 0; i < *count; i++) { + fprintf(tmp_f, " %s", search_domains[i]); + } + fprintf(tmp_f, "\n"); + + rewind(origin_f); + while (fgets(line, sizeof(line), origin_f)) { + if (strncmp(line, "search ", 7) != 0) + fprintf(tmp_f, "%s", line); + } + } + return 0; +} + +static void +update_search_domain_entries(char **search_domains, int *count, bool add) +{ + const char *resolv_conf_path = "/tmp/resolv.conf"; + char *tmppath = alloca(strlen(resolv_conf_path) + 5); + FILE *f1, *f2; + uint32_t crcold, crcnew; + + f1 = fopen(resolv_conf_path, "r"); + if (!f1) { + D(INTERFACE, "Failed to open %s for reading\n", resolv_conf_path); + return; + } + + sprintf(tmppath, "%s.tmp", resolv_conf_path); + f2 = fopen(tmppath, "w+"); + if (!f2) { + D(INTERFACE, "Failed to open %s for writing\n", tmppath); + fclose(f1); + return; + } + + if (add) { + if (write_search_domain_entries(f1, f2, search_domains, count) < 0) { + D(INTERFACE, "Failed to allocate memory for dns search domain list\n"); + fclose(f1); + fclose(f2); + return; + } + } else { + remove_search_domain_entries(f1, f2, search_domains, *count); + } + + rewind(f1); + crcold = crc32_file(f1); + fclose(f1); + + fflush(f2); + rewind(f2); + crcnew = crc32_file(f2); + fclose(f2); + + if (crcold == crcnew) { + unlink(tmppath); + } else if (rename(tmppath, resolv_conf_path) < 0) { + D(INTERFACE, "Failed to replace %s\n", resolv_conf_path); + unlink(tmppath); + } +} + +void +interface_update_search_domain_conf(struct interface *iface, bool add) +{ + char *search_domains[MAX_SEARCH_DOMAINS]; + int count = 0; + + if (iface->state != IFS_UP) + return; + + if (vlist_simple_empty(&iface->proto_ip.dns_search) && + (iface->proto_ip.no_dns || vlist_simple_empty(&iface->config_ip.dns_search))) + return; + + if (gather_interface_search_domains(&iface->config_ip, search_domains, &count) < 0) { + D(INTERFACE, "Failed to allocate memory for dns search domain list\n"); + free_search_domains(search_domains, count); + return; + } + + if (!iface->proto_ip.no_dns) { + if (gather_interface_search_domains(&iface->proto_ip, search_domains, &count) < 0) { + D(INTERFACE, "Failed to allocate memory for dns search domain list\n"); + free_search_domains(search_domains, count); + return; + } + } + + if (count > 0) { + update_search_domain_entries(search_domains, &count, add); + free_search_domains(search_domains, count); + } +} + static void interface_ip_set_route_enabled(struct interface_ip_settings *ip, struct device_route *route, bool enabled) @@ -1782,6 +1998,7 @@ interface_ip_update_start(struct interface_ip_settings *ip) void interface_ip_update_complete(struct interface_ip_settings *ip) { + interface_update_search_domain_conf(ip->iface, false); vlist_simple_flush(&ip->dns_servers); vlist_simple_flush(&ip->dns_search); vlist_flush(&ip->route); @@ -1789,6 +2006,7 @@ interface_ip_update_complete(struct interface_ip_settings *ip) vlist_flush(&ip->prefix); vlist_flush(&ip->neighbor); interface_write_resolv_conf(ip->iface->jail); + interface_update_search_domain_conf(ip->iface, true); } void @@ -1828,6 +2046,8 @@ interface_ip_init(struct interface *iface) static void interface_ip_valid_until_handler(struct uloop_timeout *t) { + char *search_domains[MAX_SEARCH_DOMAINS]; + int count = 0; time_t now = system_get_rtime(); struct interface *iface; vlist_for_each_element(&interfaces, iface, node) { @@ -1837,27 +2057,57 @@ interface_ip_valid_until_handler(struct uloop_timeout *t) struct device_addr *addr, *addrp; struct device_route *route, *routep; struct device_prefix *pref, *prefp; + struct dns_server *srv, *tmpsrv; + struct dns_search_domain *domain, *tmpdomain; + bool dns_expired = false; vlist_for_each_element_safe(&iface->proto_ip.addr, addr, node, addrp) - if (addr->valid_until && addr->valid_until < now) + if (addr->valid_until && addr->valid_until <= now) vlist_delete(&iface->proto_ip.addr, &addr->node); vlist_for_each_element_safe(&iface->proto_ip.route, route, node, routep) - if (route->valid_until && route->valid_until < now) + if (route->valid_until && route->valid_until <= now) vlist_delete(&iface->proto_ip.route, &route->node); vlist_for_each_element_safe(&iface->proto_ip.prefix, pref, node, prefp) - if (pref->valid_until && pref->valid_until < now) + if (pref->valid_until && pref->valid_until <= now) vlist_delete(&iface->proto_ip.prefix, &pref->node); + vlist_simple_for_each_element_safe(&iface->proto_ip.dns_servers, srv, node, tmpsrv) + if (srv->valid_until && srv->valid_until <= now) { + vlist_simple_delete(&iface->proto_ip.dns_servers, &srv->node); + dns_expired = true; + } + + vlist_simple_for_each_element_safe(&iface->proto_ip.dns_search, domain, node, tmpdomain) + if (domain->valid_until && domain->valid_until <= now) { + if (count < MAX_SEARCH_DOMAINS) { + search_domains[count] = strdup(domain->name); + if (!search_domains[count]) { + D(INTERFACE, "Failed to allocate memory to remove dns search domain\n"); + break; + } + ++count; + } + vlist_simple_delete(&iface->proto_ip.dns_search, &domain->node); + } + + if (dns_expired) + interface_write_resolv_conf(iface->jail); + + } + + if (count > 0) { + update_search_domain_entries(search_domains, &count, false); + free_search_domains(search_domains, count); } - uloop_timeout_set(t, 1000); + uloop_timeout_set(t, 500); } static void __init interface_ip_init_worker(void) { valid_until_timeout.cb = interface_ip_valid_until_handler; - uloop_timeout_set(&valid_until_timeout, 1000); + uloop_timeout_set(&valid_until_timeout, 500); } diff --git a/interface-ip.h b/interface-ip.h index cc7efbd..256ee81 100644 --- a/interface-ip.h +++ b/interface-ip.h @@ -162,11 +162,13 @@ struct device_source_table { struct dns_server { struct vlist_simple_node node; int af; + time_t valid_until; union if_addr addr; }; struct dns_search_domain { struct vlist_simple_node node; + time_t valid_until; char name[]; }; @@ -175,12 +177,13 @@ extern const struct uci_blob_param_list neighbor_attr_list; extern struct list_head prefixes; void interface_ip_init(struct interface *iface); -void interface_add_dns_server_list(struct interface_ip_settings *ip, struct blob_attr *list); -void interface_add_dns_search_list(struct interface_ip_settings *ip, struct blob_attr *list); void interface_write_resolv_conf(const char *jail); +void interface_update_search_domain_conf(struct interface *iface, bool add); void interface_ip_add_route(struct interface *iface, struct blob_attr *attr, bool v6); void interface_ip_add_neighbor(struct interface *iface, struct blob_attr *attr, bool v6); +void interface_add_dns_server(struct interface_ip_settings *ip, struct blob_attr *attr); +void interface_add_dns_search_domain(struct interface_ip_settings *ip, struct blob_attr *attr); void interface_ip_update_start(struct interface_ip_settings *ip); void interface_ip_update_complete(struct interface_ip_settings *ip); void interface_ip_flush(struct interface_ip_settings *ip); diff --git a/interface.c b/interface.c index 60b1807..71ab37d 100644 --- a/interface.c +++ b/interface.c @@ -520,6 +520,40 @@ interface_remove_user(struct interface_user *dep) dep->iface = NULL; } +static void +interface_add_dns_server_list(struct interface_ip_settings *ip, struct blob_attr *list) +{ + struct blob_attr *cur; + size_t rem; + + blobmsg_for_each_attr(cur, list, rem) { + if (blobmsg_type(cur) != BLOBMSG_TYPE_TABLE) + continue; + + if (!blobmsg_check_attr(cur, false)) + continue; + + interface_add_dns_server(ip, cur); + } +} + +static void +interface_add_dns_search_list(struct interface_ip_settings *ip, struct blob_attr *list) +{ + struct blob_attr *cur; + size_t rem; + + blobmsg_for_each_attr(cur, list, rem) { + if (blobmsg_type(cur) != BLOBMSG_TYPE_TABLE) + continue; + + if (!blobmsg_check_attr(cur, false)) + continue; + + interface_add_dns_search_domain(ip, cur); + } +} + static void interface_add_assignment_classes(struct interface *iface, struct blob_attr *list) { @@ -768,6 +802,7 @@ interface_proto_event_cb(struct interface_proto_state *state, enum interface_pro iface->state = IFS_UP; iface->start_time = system_get_rtime(); interface_event(iface, IFEV_UP); + interface_update_search_domain_conf(iface, true); netifd_log_message(L_NOTICE, "Interface '%s' is now up\n", iface->name); break; case IFPEV_DOWN: @@ -775,6 +810,7 @@ interface_proto_event_cb(struct interface_proto_state *state, enum interface_pro return; netifd_log_message(L_NOTICE, "Interface '%s' is now down\n", iface->name); + interface_update_search_domain_conf(iface, false); mark_interface_down(iface); interface_write_resolv_conf(iface->jail); if (iface->main_dev.dev && !(iface->config_state == IFC_NORMAL && iface->autostart && iface->available)) @@ -788,6 +824,7 @@ interface_proto_event_cb(struct interface_proto_state *state, enum interface_pro return; netifd_log_message(L_NOTICE, "Interface '%s' has lost the connection\n", iface->name); + interface_update_search_domain_conf(iface, false); mark_interface_down(iface); iface->state = IFS_SETUP; break; @@ -1361,6 +1398,7 @@ interface_change_config(struct interface *if_old, struct interface *if_new) update_prefix_delegation = true; } + interface_update_search_domain_conf(if_old, false); if_old->proto_ip.no_dns = if_new->proto_ip.no_dns; interface_replace_dns(&if_old->config_ip, &if_new->config_ip); @@ -1394,6 +1432,8 @@ interface_change_config(struct interface *if_old, struct interface *if_new) interface_update_prefix_delegation(&if_old->proto_ip); interface_write_resolv_conf(if_old->jail); + interface_update_search_domain_conf(if_old, true); + if (if_old->main_dev.dev) interface_check_state(if_old); diff --git a/proto-shell.c b/proto-shell.c index 931a59e..83d978b 100644 --- a/proto-shell.c +++ b/proto-shell.c @@ -430,6 +430,44 @@ proto_shell_parse_neighbor_list(struct interface *iface, struct blob_attr *attr, } } +static void +proto_shell_parse_dns_list(struct interface *iface, struct blob_attr *attr) +{ + struct blob_attr *cur; + size_t rem; + + blobmsg_for_each_attr(cur, attr, rem) { + if (blobmsg_type(cur) != BLOBMSG_TYPE_TABLE) { + DPRINTF("Ignore wrong dns type: %d\n", blobmsg_type(cur)); + continue; + } + + if (!blobmsg_check_attr(cur, false)) + continue; + + interface_add_dns_server(&iface->proto_ip, cur); + } +} + +static void +proto_shell_parse_dns_search_list(struct interface *iface, struct blob_attr *attr) +{ + struct blob_attr *cur; + size_t rem; + + blobmsg_for_each_attr(cur, attr, rem) { + if (blobmsg_type(cur) != BLOBMSG_TYPE_TABLE) { + DPRINTF("Ignore wrong dns search type: %d\n", blobmsg_type(cur)); + continue; + } + + if (!blobmsg_check_attr(cur, false)) + continue; + + interface_add_dns_search_domain(&iface->proto_ip, cur); + } +} + static void proto_shell_parse_data(struct interface *iface, struct blob_attr *attr) { @@ -574,10 +612,10 @@ proto_shell_update_link(struct proto_shell_state *state, struct blob_attr *data, proto_shell_parse_neighbor_list(state->proto.iface, cur, true); if ((cur = tb[NOTIFY_DNS])) - interface_add_dns_server_list(&iface->proto_ip, cur); + proto_shell_parse_dns_list(state->proto.iface, cur); if ((cur = tb[NOTIFY_DNS_SEARCH])) - interface_add_dns_search_list(&iface->proto_ip, cur); + proto_shell_parse_dns_search_list(state->proto.iface, cur); if ((cur = tb[NOTIFY_DATA])) proto_shell_parse_data(state->proto.iface, cur); diff --git a/scripts/netifd-proto.sh b/scripts/netifd-proto.sh index c25aa9f..0e37d3d 100644 --- a/scripts/netifd-proto.sh +++ b/scripts/netifd-proto.sh @@ -106,14 +106,16 @@ proto_close_data() { proto_add_dns_server() { local address="$1" + local lifetime="$2" - append PROTO_DNS "$address" + append PROTO_DNS "$address/$lifetime" } proto_add_dns_search() { local address="$1" + local lifetime="$2" - append PROTO_DNS_SEARCH "$address" + append PROTO_DNS_SEARCH "$address/$lifetime" } proto_add_ipv4_address() { @@ -302,6 +304,30 @@ _proto_push_route() { json_close_object } +_proto_push_dns() { + local str="$1"; + local dns="${str%%/*}" + str="${str#*/}" + local lifetime="${str%%/*}" + + json_add_object "" + json_add_string dns "$dns" + [ -n "$lifetime" ] && json_add_int lifetime "$lifetime" + json_close_object +} + +_proto_push_dns_search() { + local str="$1"; + local domain="${str%%/*}" + str="${str#*/}" + local lifetime="${str%%/*}" + + json_add_object "" + json_add_string domain "$domain" + [ -n "$lifetime" ] && json_add_int lifetime "$lifetime" + json_close_object +} + _proto_push_array() { local name="$1" local val="$2" @@ -332,8 +358,8 @@ proto_send_update() { _proto_push_array "routes" "$PROTO_ROUTE" _proto_push_route _proto_push_array "routes6" "$PROTO_ROUTE6" _proto_push_route _proto_push_array "ip6prefix" "$PROTO_PREFIX6" _proto_push_string - _proto_push_array "dns" "$PROTO_DNS" _proto_push_string - _proto_push_array "dns_search" "$PROTO_DNS_SEARCH" _proto_push_string + _proto_push_array "dns" "$PROTO_DNS" _proto_push_dns + _proto_push_array "dns_search" "$PROTO_DNS_SEARCH" _proto_push_dns_search _proto_push_array "neighbor" "$PROTO_NEIGHBOR" _proto_push_ipv4_neighbor _proto_push_array "neighbor6" "$PROTO_NEIGHBOR6" _proto_push_ipv6_neighbor _proto_notify "$interface" diff --git a/utils.h b/utils.h index f40e14f..3d347e3 100644 --- a/utils.h +++ b/utils.h @@ -71,6 +71,9 @@ static inline void vlist_simple_add(struct vlist_simple_tree *tree, struct vlist #define vlist_simple_for_each_element(tree, element, node_member) \ list_for_each_entry(element, &(tree)->list, node_member.list) +#define vlist_simple_for_each_element_safe(tree, element, node_member, tmp) \ + list_for_each_entry_safe(element, tmp, &(tree)->list, node_member.list) + #define vlist_simple_empty(tree) \ list_empty(&(tree)->list)