diff --git a/fail2ban/images/status.gif b/fail2ban/images/status.gif new file mode 100644 index 000000000..f6a34a259 Binary files /dev/null and b/fail2ban/images/status.gif differ diff --git a/fail2ban/index.cgi b/fail2ban/index.cgi index e6ffbfe76..a5b065858 100755 --- a/fail2ban/index.cgi +++ b/fail2ban/index.cgi @@ -28,14 +28,14 @@ if ($err) { # Show category icons my @links = ( "list_filters.cgi", "list_actions.cgi", - "list_jails.cgi", "edit_config.cgi", - "edit_manual.cgi", ); + "list_jails.cgi", "list_status.cgi", + "edit_config.cgi", "edit_manual.cgi", ); my @titles = ( $text{'filters_title'}, $text{'actions_title'}, - $text{'jails_title'}, $text{'config_title'}, - $text{'manual_title'}, ); + $text{'jails_title'}, $text{'status_title'}, + $text{'config_title'}, $text{'manual_title'}, ); my @icons = ( "images/filters.gif", "images/actions.gif", - "images/jails.gif", "images/config.gif", - "images/manual.gif", ); + "images/jails.gif", "images/status.gif", + "images/config.gif", "images/manual.gif", ); &icons_table(\@links, \@titles, \@icons, 5); # Show start / stop buttons diff --git a/fail2ban/list_status.cgi b/fail2ban/list_status.cgi new file mode 100755 index 000000000..3543934b3 --- /dev/null +++ b/fail2ban/list_status.cgi @@ -0,0 +1,102 @@ +#!/usr/local/bin/perl +# Show a list of all defined actions + +use strict; +use warnings; +require './fail2ban-lib.pl'; +our (%in, %text, %config); + +&ui_print_header(undef, $text{'status_title2'}, ""); + +# Check if firewalld is used +&foreign_require('firewalld', 'install_check.pl'); +my $is_firewalld = &firewalld::is_installed(); + +my $out = &backquote_logged("$config{'client_cmd'} status 2>&1 $limit) { + my @ips = split($br, $ips); + @ips = @ips[0 .. $limit]; + $ips = join($br, @ips); + $ips .= "$br$nbsp".&text('list_rules_plus_more', $ipscount-$limit).""; + } + return $ips; + }; + my $jips; + my $noval; + &open_execute_command($fh, $jcmd, 1); + while(<$fh>) { + if (/-\s+(.*):\s*(.*)/) { + my $col = $1; + my $val = $2; + $col = lc($col); + $col =~ s/\s/_/g; + if ($col !~ /journal_matches/) { + push(@head, "
".$text{"status_head_$col"}."
"); + if ($col =~ /banned_ip_list/) { + $jips = $val; + my @ips = split(/\s+/, $val); + @ips = map { "" } @ips; + $val = "
" if ($val); + $val .= join('
', @ips); + $val = &$ipslimit($val); + $val .= "

" if ($val); + $val .= "–", $noval++ if (!$val); + } + push(@body, $val); + } + } + } + close($fh); + if (!$head++) { + print &ui_form_start("unblock_jail.cgi", "post"); + print &ui_links_row(\@links); + print &ui_columns_start(\@head); + } + print &ui_checked_columns_row(\@body, [ 'width=5', undef, $tdc, $tdc, $tdc, $tdc, $noval ? $tdc : undef ], "jail", $jail); + push(@jipsall, ["$jail" => $jips]); + } + if ($head) { + print &ui_columns_end(); + print &ui_links_row(\@links); + foreach my $j (@jipsall) { + print &ui_hidden("jips-$j->[0]", "$j->[1]"); + } + print &ui_form_end([ [ 'unblock', $text{'status_jail_unblock'} ], + $is_firewalld ? + [ 'permblock', $text{'status_jail_block'} ] : undef ]); + } +} +else { + print $text{'status_jail_noactive'}; + } + +&ui_print_footer("", $text{'index_return'}); diff --git a/fail2ban/unblock_jail.cgi b/fail2ban/unblock_jail.cgi new file mode 100755 index 000000000..7037d1828 --- /dev/null +++ b/fail2ban/unblock_jail.cgi @@ -0,0 +1,54 @@ +#!/usr/local/bin/perl +# Create, update or delete a action + +use strict; +use warnings; +require './fail2ban-lib.pl'; +our (%in, %text, %config); +&ReadParse(); +&error_setup($text{'status_err_set'}); + +my @jails = split(/\0/, $in{'jail'}); +my $action = $in{'permblock'} ? 'block' : $in{'unblock'} ? 'unblock' : undef; + +# Error checks +!$action || $in{'jail'} || &error($text{'status_err_nojail'}); + +# Unblock given IP in given jail +my $unblock_jailed_ip = sub { + my ($jail, $ip) = @_; + my $cmd = "$config{'client_cmd'} set ".quotemeta($jail)." unbanip ".quotemeta($ip)." 2>&1 &1"); -return $? ? $out : undef; +&foreign_require("init"); +my ($ok, $err) = &init::restart_action($config{'init_name'}); +return $ok ? undef : $err; } # stop_firewalld() @@ -353,4 +354,147 @@ else { } } +# get_default_zone +# Returns default zone +sub get_default_zone +{ +my @zones = &list_firewalld_zones(); +my ($zone) = grep { $_->{'default'} } @zones; +return $zone; +} + +# add_ip_ban(ip, [zone]) +# Ban given IP address in given or default zone +sub add_ip_ban +{ +my ($ip, $zone) = @_; +return create_rich_rule('add', $ip, $zone); +} + +# remove_ip_ban(ip, [zone]) +# Un-ban given IP address in given or default zone +sub remove_ip_ban +{ +my ($ip, $zone) = @_; +return create_rich_rule('remove', $ip, $zone); +} + +# create_rich_rule(action, ip, [\zone], [opts]) +# Add or remove rich rule for given IP in given or default zone +sub create_rich_rule +{ +my ($action, $ip, $zone, $opts) = @_; +my $ip_validate = sub { + return &check_ipaddress($_[0]) || &check_ip6address($_[0]); + }; + +# Default action for permanent ban is 'drop' +my $action_type = "drop"; + +# Override defaults +if (ref($opts)) { + + # Override default action + $action_type = lc($opts->{'action'}) + if ($opts->{'action'} && + $opts->{'action'} =~ /^accept|reject|drop|mark$/); +} + +# Zone name +if (!$zone) { + ($zone) = get_default_zone(); + } +$zone = &sanitize_zone_name($zone->{'name'}); + +# Validate action +$action eq 'add' || $action eq 'remove' || &error($text{'list_rule_actionerr'}); + +# Validate IP +&$ip_validate($ip) || &error($text{'list_rule_iperr'}); + +# Set family +my $family = $ip =~ /:/ ? 'ipv6' : 'ipv4'; + +# Apply block +# (quotemeta doesn't work for params) +my $get_cmd = sub { + my ($rtype) = @_; + my $type; + $type = " --permanent" if ($rtype eq 'permanent'); + return "$config{'firewall_cmd'} --zone=".$zone."$type --$action-rich-rule=\"rule family='$family' source address='$ip' $action_type\""; + }; +my $out = &backquote_logged(&$get_cmd()." 2>&1 &1 {'name'}); + +# Sanitize rule +$rule = &sanitize_rule_name($rule); + +# Remove rule command +# (quotemeta doesn't work for params) +my $get_cmd = sub { + my ($rtype) = @_; + my $type; + $type = " --permanent" if ($rtype eq 'permanent'); + return "$config{'firewall_cmd'} --zone=${zone}${type} --remove-rich-rule '${rule}'"; + }; + +my $out = &backquote_logged(&$get_cmd()." 2>&1 &1 &1 &1 {'name'} eq $in{'zone'} } @zones; - } -else { - ($zone) = grep { $_->{'default'} } @zones; - } -$zone ||= $zones[0]; -my ($azone); -eval { - local $main::error_must_die = 1; - my @azones = &list_firewalld_zones(1); - ($azone) = grep { $_->{'name'} eq $zone->{'name'} } @azones; - }; -# Show zone selector -print &ui_form_start("index.cgi"); -print "$text{'index_zone'}  ", - &ui_select("zone", $zone->{'name'}, - [ map { [ $_->{'name'}, - $_->{'name'}.($_->{'default'} ? ' (default)' : '') ]} - @zones ], 1, 0, 0, 0, - "onChange='form.submit()'")," ", - &ui_submit($text{'index_zonedef'}, "defzone")," ", - &ui_submit($text{'index_zonedel'}, "delzone"),"   ", - &ui_submit($text{'index_zoneadd'}, "addzone")," ", - "

\n"; -print &ui_form_end(); - -# Show allowed ports and services in this zone -my @links = ( &ui_link("edit_port.cgi?new=1&zone=".&urlize($zone->{'name'}), - $text{'index_padd'}), - &ui_link("edit_serv.cgi?new=1&zone=".&urlize($zone->{'name'}), - $text{'index_sadd'}), - &ui_link("edit_forward.cgi?new=1&zone=".&urlize($zone->{'name'}), - $text{'index_fadd'}), - ); -if (@{$zone->{'services'}} || @{$zone->{'ports'}}) { - my @tds = ( "width=5" ); - unshift(@links, &select_all_link("d", 1), - &select_invert_link("d", 1)); - print &ui_form_start("delete_rules.cgi", "post"); - print &ui_hidden("zone", $zone->{'name'}); - print &ui_links_row(\@links); - print &ui_columns_start([ "", $text{'index_type'}, $text{'index_port'}, - $text{'index_proto'} ], 100, 0, \@tds); - foreach my $s (@{$zone->{'services'}}) { - my $url = "edit_serv.cgi?id=".&urlize($s). - "&zone=".&urlize($zone->{'name'}); - my $sportsprotos = &list_firewalld_service_desc($s); - my $sport = $sportsprotos->{'ports'}; - my $sprotocols = $sportsprotos->{'protocols'}; - $sport = " ($sport)" if ($sport); - print &ui_checked_columns_row([ - &ui_link($url, $text{'index_tservice'}), - &ui_link($url, "$s$sport"), - $sprotocols || "", - ], \@tds, "d", "service/".$s); - } - foreach my $p (@{$zone->{'ports'}}) { - my $url = "edit_port.cgi?id=".&urlize($p). - "&zone=".&urlize($zone->{'name'}); - my ($port, $proto) = split(/\//, $p); - print &ui_checked_columns_row([ - &ui_link($url, $text{'index_tport'}), - &ui_link($url, $port), - uc($proto), - ], \@tds, "d", "port/".$p); - } - foreach my $f (@{$zone->{'forward-ports'}}) { - my ($port, $proto, $dstport, $dstaddr) = - &parse_firewalld_forward($f); - my $p = join("/", $port, $proto, $dstport, $dstaddr); - my $url = "edit_forward.cgi?id=".&urlize($p). - "&zone=".&urlize($zone->{'name'}); - print &ui_checked_columns_row([ - &ui_link($url, $text{'index_tforward'}), - &ui_link($url, $port), - &ui_link($url, uc($proto)), - ], \@tds, "d", "forward/".$p); - } - print &ui_columns_end(); - print &ui_links_row(\@links); - print &ui_form_end([ [ undef, $text{'index_delete'} ] ]); - } -else { - print "$text{'index_none'}

\n"; - print &ui_links_row(\@links); - } - -if ($azone) { - # Show interfaces for this zone - print &ui_form_start("save_ifaces.cgi"); - print &ui_hidden("zone", $zone->{'name'}); - print "
$text{'index_ifaces'} \n"; - my %zifcs = map { $_, 1 } &unique(@{$azone->{'interfaces'}}, - @{$zone->{'interfaces'}}); - print &ui_radio("iface_def", %zifcs ? 0 : 1, - [ [ 1, $text{'index_ifaces_def'} ], - [ 0, $text{'index_ifaces_sel'} ] ]),"\n"; - foreach my $i (&list_system_interfaces()) { - print &ui_checkbox("iface", $i, $i, $zifcs{$i}),"\n"; - } - print &ui_submit($text{'save'}); - print &ui_form_end(); - } - -# Show start/apply buttons -print &ui_hr(); -print &ui_buttons_start(); +# Is FirewallD running if not, show start button my $ok = &is_firewalld_running(); if ($ok) { - print &ui_buttons_row("restart.cgi", $text{'index_restart'}, - $text{'index_restartdesc'}, + # Get rules and zones + @zones = &list_firewalld_zones(); + @zones || &error($text{'index_ezones'}); + if ($in{'zone'}) { + ($zone) = grep { $_->{'name'} eq $in{'zone'} } @zones; + } + else { + ($zone) = grep { $_->{'default'} } @zones; + } + $zone ||= $zones[0]; + my ($azone); + eval { + local $main::error_must_die = 1; + my @azones = &list_firewalld_zones(1); + ($azone) = grep { $_->{'name'} eq $zone->{'name'} } @azones; + }; + + # Show zone selector + print &ui_form_start("index.cgi"); + print "$text{'index_zone'}  ", + &ui_select("zone", $zone->{'name'}, + [ map { [ $_->{'name'}, + $_->{'name'}.($_->{'default'} ? ' (default)' : '') ]} + @zones ], 1, 0, 0, 0, + "onChange='form.submit()'")," ", + &ui_submit($text{'index_zonedef'}, "defzone")," ", + &ui_submit($text{'index_zonedel'}, "delzone"),"   ", + &ui_submit($text{'index_zoneadd'}, "addzone")," ", + "

\n"; + print &ui_form_end(); + + # Show allowed ports and services in this zone + my @links = ( &ui_link("edit_port.cgi?new=1&zone=".&urlize($zone->{'name'}), + $text{'index_padd'}), + &ui_link("edit_serv.cgi?new=1&zone=".&urlize($zone->{'name'}), + $text{'index_sadd'}), + &ui_link("edit_forward.cgi?new=1&zone=".&urlize($zone->{'name'}), + $text{'index_fadd'}), + ); + if (@{$zone->{'services'}} || @{$zone->{'ports'}}) { + my @tds = ( "width=5" ); + unshift(@links, &select_all_link("d", 1), + &select_invert_link("d", 1)); + print &ui_form_start("delete_rules.cgi", "post"); + print &ui_hidden("zone", $zone->{'name'}); + print &ui_links_row(\@links); + print &ui_columns_start([ "", $text{'index_type'}, $text{'index_port'}, + $text{'index_proto'} ], 100, 0, \@tds); + foreach my $s (@{$zone->{'services'}}) { + my $url = "edit_serv.cgi?id=".&urlize($s). + "&zone=".&urlize($zone->{'name'}); + my $sportsprotos = &list_firewalld_service_desc($s); + my $sport = $sportsprotos->{'ports'}; + my $sprotocols = $sportsprotos->{'protocols'}; + $sport = " ($sport)" if ($sport); + print &ui_checked_columns_row([ + &ui_link($url, $text{'index_tservice'}), + &ui_link($url, "$s$sport"), + $sprotocols || "", + ], \@tds, "d", "service/".$s); + } + foreach my $p (@{$zone->{'ports'}}) { + my $url = "edit_port.cgi?id=".&urlize($p). + "&zone=".&urlize($zone->{'name'}); + my ($port, $proto) = split(/\//, $p); + print &ui_checked_columns_row([ + &ui_link($url, $text{'index_tport'}), + &ui_link($url, $port), + uc($proto), + ], \@tds, "d", "port/".$p); + } + foreach my $f (@{$zone->{'forward-ports'}}) { + my ($port, $proto, $dstport, $dstaddr) = + &parse_firewalld_forward($f); + my $p = join("/", $port, $proto, $dstport, $dstaddr); + my $url = "edit_forward.cgi?id=".&urlize($p). + "&zone=".&urlize($zone->{'name'}); + print &ui_checked_columns_row([ + &ui_link($url, $text{'index_tforward'}), + &ui_link($url, $port), + &ui_link($url, uc($proto)), + ], \@tds, "d", "forward/".$p); + } + print &ui_columns_end(); + print &ui_links_row(\@links); + print &ui_form_end([ [ undef, $text{'index_delete'} ] ]); + } + else { + print "$text{'index_none'}

\n"; + print &ui_links_row(\@links); + } + + if ($azone) { + # Show interfaces for this zone + print &ui_form_start("save_ifaces.cgi"); + print &ui_hidden("zone", $zone->{'name'}); + print "
$text{'index_ifaces'} \n"; + my %zifcs = map { $_, 1 } &unique(@{$azone->{'interfaces'}}, + @{$zone->{'interfaces'}}); + print &ui_radio("iface_def", %zifcs ? 0 : 1, + [ [ 1, $text{'index_ifaces_def'} ], + [ 0, $text{'index_ifaces_sel'} ] ]),"\n"; + foreach my $i (&list_system_interfaces()) { + print &ui_checkbox("iface", $i, $i, $zifcs{$i}),"\n"; + } + print &ui_submit($text{'index_ifaces_apply'}); + print &ui_form_end(); + } + + print &ui_hr(); + + # Show start/apply buttons + print &ui_buttons_start(); + print &ui_buttons_row("list_rules.cgi", $text{'index_listrules'}, + &text("index_listrules_restartdesc", + "".$zone->{'name'}.""), + [ [ "zone", $zone->{'name'} ] ]); + print &ui_buttons_row("restart.cgi", $text{'index_restart_firewalld'}, + $text{'index_restart_firewallddesc'}, [ [ "zone", $zone->{'name'} ] ]); print &ui_buttons_row("stop.cgi", $text{'index_stop'}, $text{'index_stopdesc'}, [ [ "zone", $zone->{'name'} ] ]); - } -else { +} + +# Show Start and disable/enabled at boot button +if (!$ok) { + print &ui_buttons_start(); print &ui_buttons_row("start.cgi", $text{'index_start'}, $text{'index_startdesc'}, [ [ "zone", $zone->{'name'} ] ]); diff --git a/firewalld/list_rules.cgi b/firewalld/list_rules.cgi new file mode 100755 index 000000000..a97f98fb2 --- /dev/null +++ b/firewalld/list_rules.cgi @@ -0,0 +1,182 @@ +#!/usr/local/bin/perl +# List FirewallD rich and direct rules + +use strict; +use warnings; +require './firewalld-lib.pl'; +our (%in, %text, %config); +&ReadParse(); +my $dzone = $in{'zone'}; +if (!$dzone) { + my $zone = &get_default_zone(); + $dzone = $zone->{'name'}; + } +&ui_print_header(&text('list_rules_title_sub', "".&html_escape($dzone).""), $text{'list_rules_title'}, ""); + +my $head; +my @head = (undef, $text{'list_rules_type'}); +my $tdc = "style=\"text-align: center\""; +my @links = ( &select_all_link("rules"), + &select_invert_link("rules") ); + +# Check rich rules first +my $fh = 'rrules'; +my $rcmd = "$config{'firewall_cmd'} --list-rich-rules --zone=$dzone"; +&open_execute_command($fh, "$rcmd 2>&1 ) { + my @body; + if ($_ =~ /\S+/) { + push(@body, $text{'list_rules_type_rich'}); + + # Get protocol + if (/family=["'](ipv\d)["']/) { + push(@head, $text{'list_rules_protocol'}); + push(@body, $1 =~ /ipv6/i ? "IPv6" : "IPv4"); + } + + # Get address + if (/address=["'](.*?)["']/) { + push(@head, $text{'list_rules_ip'}); + push(@body, "$1  "); + } + + # Get origin + if (/\s+(source|destination)\s+/) { + push(@head, $text{'list_rules_origin'}); + push(@body, $1 eq 'source' ? 'Input' : 'Output'); + } + + # Get action + if (/(accept|reject|drop|mark$)/i) { + push(@head, $text{'list_rules_action'}); + push(@body, ucfirst($1)); + } + + # Add full rule + push(@head, $text{'list_rules_rule'}); + push(@body, "$_"); + + # Print start + if (!$head++) { + print &ui_form_start("save_rules.cgi", "post"); + print &ui_hidden("zone", $dzone); + print &ui_links_row(\@links); + print &ui_columns_start(\@head); + } + print &ui_checked_columns_row(\@body, [ 'width=5', $tdc, $tdc, undef, $tdc, $tdc, undef ], "rules", $_); + } + } +close($fh); + +# Check direct rules +my $fh2 = 'drules'; +my $dcmd = "$config{'firewall_cmd'} --direct --get-all-rules"; +&open_execute_command($fh2, "$dcmd 2>&1 ) { + my @body; + if ($_ =~ /\S+/) { + my $ndash = "–"; + my $br = "
"; + my $nbsp = " "; + my $ips = $ndash; + my $candelete = 1; + my $ipslimit = sub { + my ($ips, $limit) = @_; + $limit ||= 15; + # Limit sanity check and adjustment + $limit = 1 if ($limit < 1); + $limit -= 1; + my $ipscount = () = $ips =~ /$br/g; + if ($ipscount > $limit) { + my @ips = split($br, $ips); + @ips = @ips[0 .. $limit]; + $ips = join($br, @ips); + $ips .= "$br$nbsp".&text('list_rules_plus_more', $ipscount-$limit).""; + } + return $ips; + }; + # Extract IPs from match sets + if (/set\s+\-\-match-set\s+(.*?)\s+/) { + my $ipset_name = $1; + my $ipset_cmd = &has_command($config{'firewall_ipset'} || 'ipset'); + my $ipset_cmd_out = &backquote_logged("$ipset_cmd list ".quotemeta($ipset_name)." 2>&1 0) { + my @ipset_cmd_out_lines = split(/\n/, $ipset_cmd_out); + my @ips = map { $_ =~ /^([0-9\.\:a-f\/]+)/i } @ipset_cmd_out_lines; + $ips = join("$nbsp$nbsp$br", @ips); + } + } + } + # Rules with match sets must not be controlled here + $candelete = 0; + } + + # Standard direct rules + else { + # Extract IPs from the rule, + # considering comma separated + my @ips = ($_ =~ /-[sd]\s+([0-9\.\:a-f,\/]+)/gi); + $ips = join("$nbsp$nbsp$br", @ips); + $ips =~ s/\s*,\s*/$nbsp$nbsp$br/g; + $ips ||= $ndash; + } + + # Trim the number of IPs to allow at max 10 + $ips = &$ipslimit($ips); + + # Add type name + push(@body, $text{'list_rules_type_direct'}); + + # Get protocol + if (/(ipv\d)/) { + push(@head, $text{'list_rules_protocol'}); + push(@body, $1 =~ /ipv6/i ? "IPv6" : "IPv4"); + } + + # Get address + if (/address=["'](.*?)["']/) { + } + push(@head, $text{'list_rules_ip'}); + push(@body, $ips); + + # Get origin + if (/(INPUT|OUTPUT)/) { + push(@head, $text{'list_rules_origin'}); + push(@body, ucfirst(lc($1))); + } + + # Get action + if (/(ACCEPT|REJECT|DROP|MARK$)/) { + push(@head, $text{'list_rules_action'}); + push(@body, ucfirst(lc($1))); + } + + # Add full rule + push(@head, $text{'list_rules_rule'}); + push(@body, "$_"); + + # Print start + if (!$head++) { + print &ui_form_start("save_rules.cgi", "post"); + print &ui_hidden("zone", $dzone); + print &ui_links_row(\@links); + print &ui_columns_start(\@head); + } + print &ui_checked_columns_row(\@body, [ 'width=5', $tdc, $tdc, undef, $tdc, $tdc, undef ], "rules", $_, undef, !$candelete); + } + } +close($fh2); + + +if ($head) { + print &ui_columns_end(); + print &ui_links_row(\@links); + print &ui_form_end([ [ 'remove', $text{'list_rules_delete'} ] ] ); + } +else { + print "There are no existing direct or rich firewall rules to display." + } + +&ui_print_footer("index.cgi?zone=".&urlize($dzone), $text{'index_return'}); diff --git a/firewalld/save_rules.cgi b/firewalld/save_rules.cgi new file mode 100755 index 000000000..055857060 --- /dev/null +++ b/firewalld/save_rules.cgi @@ -0,0 +1,26 @@ +#!/usr/local/bin/perl +# Delete multiple ports or services + +use strict; +use warnings; +require './firewalld-lib.pl'; +our (%in, %text); +&error_setup($text{'delete_err'}); +&ReadParse(); +my @rules = split(/\0/, $in{'rules'}); +@rules || &error($text{'delete_enone'}); + +my @zones = &list_firewalld_zones(); +my ($zone) = grep { $_->{'name'} eq $in{'zone'} } @zones; +$zone || &error($text{'port_ezone'}); + +if ($in{'remove'}) { + foreach my $rule (@rules) { + my $rrfunc = \&{"remove_" . ($rule =~ /^(ipv4|ipv6|eb)/ ? 'direct' : 'rich') . "_rule"}; + my $rmerr = &$rrfunc($rule, $zone); + &error(&text('delete_edel', $rule, $rmerr)) if ($rmerr); + } + } + +&webmin_log("save", "rules", scalar(@rules)); +&redirect("list_rules.cgi?zone=".&urlize($zone->{'name'}));