#!/usr/bin/perl # setup.cgi # Create a Webmin-managed nftables profile table require './nftables-lib.pl'; ## no critic use strict; use warnings; our (%in, %text); ReadParse(); error_setup($text{'setup_err'}); if ($in{'action'} eq 'create') { my $profile = $in{'profile'} || 'virtualmin'; my $table_name = $in{'table_name'} || default_profile_table_name(); $table_name =~ /^\w[\w-]*$/ || error($text{'create_ename'}); my @tables = get_nftables_save(); foreach my $t (@tables) { if ($t->{'family'} eq 'inet' && $t->{'name'} eq $table_name) { error(text('setup_edup', nft_table_spec($t))); } } my ($active, $active_err) = get_active_nftables_save(); if (!$active_err) { foreach my $t (@$active) { if ($t->{'family'} eq 'inet' && $t->{'name'} eq $table_name && table_is_externally_managed($t)) { error(text('create_eexternal', nft_table_spec($t))); } } } my @allow = grep { $_ ne '' } split(/\0/, $in{'allow'} || ''); my $table = create_profile_ruleset($profile, $table_name, \@allow); push(@tables, $table); my $error = save_configuration(@tables); if ($error) { error(text('setup_failed', $error)); } $error = apply_restore(); if ($error) { error(text('setup_failed', $error)); } webmin_log("setup", "create", $profile, { 'family' => 'inet', 'table' => $table_name }); redirect("index.cgi?table_family=inet&table_name=".urlize($table_name)); return; } ui_print_header(undef, $text{'setup_title'}, "", "intro", 1, 1, undef, restart_button()); print ui_form_start("setup.cgi"); print ui_hidden("action", "create"); my @profiles = setup_profiles(); my $profile = $in{'profile'} || 'virtualmin'; my %profile_map = map { $_->{'id'} => $_ } @profiles; $profile = 'virtualmin' if (!$profile_map{$profile}); my %checked = map { $_ => 1 } @{$profile_map{$profile}->{'services'} || [ ]}; my @profile_opts = map { [ $_->{'id'}, $_->{'name'} ] } @profiles; print ui_table_start($text{'setup_header'}, "width=100%", 2); print ui_table_row($text{'setup_table_name'}, ui_textbox("table_name", $in{'table_name'} || profile_table_name($profile), 24)); print ui_table_row($text{'setup_profile'}, ui_select("profile", $profile, \@profile_opts, 1, 0, 0, 0). ui_tag('div', ui_note($profile_map{$profile}->{'desc'}, 0), { 'id' => 'nftables_profile_note', 'style' => 'margin-top: 0.35em; margin-left: 0.15em;' })); print ui_table_end(); my @services = setup_services(); my @links = ( select_all_link("allow", 0), select_invert_link("allow", 0) ); print ui_hr(); print ui_links_row(\@links); my @tds = ( "width=5" ); print ui_columns_start( [ "", $text{'setup_service_col'}, $text{'setup_type_col'}, $text{'setup_port_col'}, $text{'setup_proto_col'} ], 100, 0, \@tds, $text{'setup_services'}); foreach my $svc (sort { lc($a->{'label'}) cmp lc($b->{'label'}) } @services) { print ui_checked_columns_row([ $svc->{'label'}, $svc->{'type'}, $svc->{'port'}, $svc->{'proto'}, ], \@tds, "allow", $svc->{'id'}, $checked{$svc->{'id'}}); } print ui_columns_end(); print profile_javascript(@profiles); print ui_form_end([ [ undef, $text{'setup_create'} ] ]); ui_print_footer("index.cgi", $text{'index_return'}); sub setup_profiles { return ( { 'id' => 'allow_all', 'name' => $text{'setup_profile_allow_all'}, 'desc' => $text{'setup_profile_allow_all_desc'}, 'input' => 'accept', 'forward' => 'accept', 'output' => 'accept', 'services' => [ ] }, { 'id' => 'management', 'name' => $text{'setup_profile_management'}, 'desc' => $text{'setup_profile_management_desc'}, 'input' => 'drop', 'forward' => 'drop', 'output' => 'accept', 'services' => [ qw(ssh webmin) ] }, { 'id' => 'web', 'name' => $text{'setup_profile_web'}, 'desc' => $text{'setup_profile_web_desc'}, 'input' => 'drop', 'forward' => 'drop', 'output' => 'accept', 'services' => [ qw(ssh webmin http https) ] }, { 'id' => 'mail', 'name' => $text{'setup_profile_mail'}, 'desc' => $text{'setup_profile_mail_desc'}, 'input' => 'drop', 'forward' => 'drop', 'output' => 'accept', 'services' => [ qw(ssh usermin smtp submission smtps pop3 pop3s imap imaps) ] }, { 'id' => 'dns', 'name' => $text{'setup_profile_dns'}, 'desc' => $text{'setup_profile_dns_desc'}, 'input' => 'drop', 'forward' => 'drop', 'output' => 'accept', 'services' => [ qw(ssh webmin dhcpv6 dns dot mdns) ] }, { 'id' => 'virtualmin', 'name' => $text{'setup_profile_virtualmin'}, 'desc' => $text{'setup_profile_virtualmin_desc'}, 'input' => 'drop', 'forward' => 'drop', 'output' => 'accept', 'services' => [ qw(ssh webmin dhcpv6 dns dot ftp http https imap imaps mdns pop3 pop3s smtp submission smtps ftp_data ssh_alt webmin_range usermin passive_ftp) ] }, { 'id' => 'locked', 'name' => $text{'setup_profile_locked'}, 'desc' => $text{'setup_profile_locked_desc'}, 'input' => 'drop', 'forward' => 'drop', 'output' => 'drop', 'services' => [ ] }, { 'id' => 'custom', 'name' => $text{'setup_profile_custom'}, 'desc' => $text{'setup_profile_custom_desc'}, 'input' => 'drop', 'forward' => 'drop', 'output' => 'accept', 'services' => [ ] }, ); } sub setup_services { my $webmin_port = get_webmin_port(); my $usermin_port = get_usermin_port(); return ( { 'id' => 'ssh', 'label' => $text{'setup_svc_ssh'}, 'type' => $text{'setup_type_service'}, 'port' => '22', 'proto' => 'TCP', 'rules' => [ 'tcp dport 22 accept' ] }, { 'id' => 'webmin', 'label' => text('setup_svc_webmin', $webmin_port), 'type' => $text{'setup_type_service'}, 'port' => $webmin_port, 'proto' => 'TCP', 'rules' => [ "tcp dport $webmin_port accept" ] }, { 'id' => 'dhcpv6', 'label' => $text{'setup_svc_dhcpv6'}, 'type' => $text{'setup_type_service'}, 'port' => '546', 'proto' => 'UDP', 'rules' => [ 'ip6 daddr fe80::/64 udp dport 546 accept' ] }, { 'id' => 'dns', 'label' => $text{'setup_svc_dns'}, 'type' => $text{'setup_type_service'}, 'port' => '53', 'proto' => 'TCP/UDP', 'rules' => [ 'tcp dport 53 accept', 'udp dport 53 accept' ] }, { 'id' => 'dot', 'label' => $text{'setup_svc_dot'}, 'type' => $text{'setup_type_service'}, 'port' => '853', 'proto' => 'TCP', 'rules' => [ 'tcp dport 853 accept' ] }, { 'id' => 'ftp', 'label' => $text{'setup_svc_ftp'}, 'type' => $text{'setup_type_service'}, 'port' => '21', 'proto' => 'TCP', 'rules' => [ 'tcp dport 21 accept' ] }, { 'id' => 'http', 'label' => $text{'setup_svc_http'}, 'type' => $text{'setup_type_service'}, 'port' => '80', 'proto' => 'TCP', 'rules' => [ 'tcp dport 80 accept' ] }, { 'id' => 'https', 'label' => $text{'setup_svc_https'}, 'type' => $text{'setup_type_service'}, 'port' => '443', 'proto' => 'TCP', 'rules' => [ 'tcp dport 443 accept' ] }, { 'id' => 'imap', 'label' => $text{'setup_svc_imap'}, 'type' => $text{'setup_type_service'}, 'port' => '143', 'proto' => 'TCP', 'rules' => [ 'tcp dport 143 accept' ] }, { 'id' => 'imaps', 'label' => $text{'setup_svc_imaps'}, 'type' => $text{'setup_type_service'}, 'port' => '993', 'proto' => 'TCP', 'rules' => [ 'tcp dport 993 accept' ] }, { 'id' => 'mdns', 'label' => $text{'setup_svc_mdns'}, 'type' => $text{'setup_type_service'}, 'port' => '5353', 'proto' => 'UDP', 'rules' => [ 'ip daddr 224.0.0.251 udp dport 5353 accept', 'ip6 daddr ff02::fb udp dport 5353 accept' ] }, { 'id' => 'pop3', 'label' => $text{'setup_svc_pop3'}, 'type' => $text{'setup_type_service'}, 'port' => '110', 'proto' => 'TCP', 'rules' => [ 'tcp dport 110 accept' ] }, { 'id' => 'pop3s', 'label' => $text{'setup_svc_pop3s'}, 'type' => $text{'setup_type_service'}, 'port' => '995', 'proto' => 'TCP', 'rules' => [ 'tcp dport 995 accept' ] }, { 'id' => 'smtp', 'label' => $text{'setup_svc_smtp'}, 'type' => $text{'setup_type_service'}, 'port' => '25', 'proto' => 'TCP', 'rules' => [ 'tcp dport 25 accept' ] }, { 'id' => 'submission', 'label' => $text{'setup_svc_submission'}, 'type' => $text{'setup_type_service'}, 'port' => '587', 'proto' => 'TCP', 'rules' => [ 'tcp dport 587 accept' ] }, { 'id' => 'smtps', 'label' => $text{'setup_svc_smtps'}, 'type' => $text{'setup_type_service'}, 'port' => '465', 'proto' => 'TCP', 'rules' => [ 'tcp dport 465 accept' ] }, { 'id' => 'ftp_data', 'label' => $text{'setup_port_ftp_data'}, 'type' => $text{'setup_type_port'}, 'port' => '20', 'proto' => 'TCP', 'rules' => [ 'tcp dport 20 accept' ] }, { 'id' => 'ssh_alt', 'label' => $text{'setup_port_ssh_alt'}, 'type' => $text{'setup_type_port'}, 'port' => '2222', 'proto' => 'TCP', 'rules' => [ 'tcp dport 2222 accept' ] }, { 'id' => 'webmin_range', 'label' => $text{'setup_port_webmin_range'}, 'type' => $text{'setup_type_port'}, 'port' => '10000-10100', 'proto' => 'TCP', 'rules' => [ 'tcp dport 10000-10100 accept' ] }, { 'id' => 'usermin', 'label' => $text{'setup_port_usermin'}, 'type' => $text{'setup_type_port'}, 'port' => $usermin_port, 'proto' => 'TCP', 'rules' => [ "tcp dport $usermin_port accept" ] }, { 'id' => 'passive_ftp', 'label' => $text{'setup_port_passive_ftp'}, 'type' => $text{'setup_type_port'}, 'port' => '49152-65535', 'proto' => 'TCP', 'rules' => [ 'tcp dport 49152-65535 accept' ] }, ); } sub create_profile_ruleset { my ($profile_id, $table_name, $allow_ids) = @_; my %profiles = map { $_->{'id'} => $_ } setup_profiles(); my $profile = $profiles{$profile_id} || error($text{'setup_invalid_type'}); my @services = setup_services(); my %services = map { $_->{'id'} => $_ } @services; my %allow; foreach my $id (@$allow_ids) { $services{$id} || error(text('setup_eservice', $id)); $allow{$id} = 1; } my $table = { 'name' => $table_name, 'family' => 'inet', 'rules' => [ ], 'sets' => { }, 'chains' => { 'input' => { 'type' => 'filter', 'hook' => 'input', 'priority' => 0, 'policy' => $profile->{'input'} }, 'forward' => { 'type' => 'filter', 'hook' => 'forward', 'priority' => 0, 'policy' => $profile->{'forward'} }, 'output' => { 'type' => 'filter', 'hook' => 'output', 'priority' => 0, 'policy' => $profile->{'output'} } } }; return $table if ($profile_id eq 'allow_all'); add_profile_rule($table, 'input', 'ct state established,related accept'); add_profile_rule($table, 'input', 'iif "lo" accept'); add_profile_rule($table, 'input', 'meta l4proto { icmp, ipv6-icmp } accept'); if ($profile->{'output'} eq 'drop') { add_profile_rule($table, 'output', 'ct state established,related accept'); add_profile_rule($table, 'output', 'oif "lo" accept'); add_profile_rule($table, 'output', 'meta l4proto { icmp, ipv6-icmp } accept'); } my %seen; foreach my $id (map { $_->{'id'} } @services) { next if (!$allow{$id}); foreach my $rule (@{$services{$id}->{'rules'}}) { next if ($seen{$rule}++); add_profile_rule($table, 'input', $rule); } } return $table; } sub profile_javascript { my (@profiles) = @_; my %profile_services = map { $_->{'id'} => $_->{'services'} } @profiles; my %profile_tables = map { $_->{'id'} => profile_table_name($_->{'id'}) } @profiles; my %profile_notes = map { $_->{'id'} => ui_note($_->{'desc'}, 0) } @profiles; my $json = convert_to_json(\%profile_services); my $table_json = convert_to_json(\%profile_tables); my $note_json = convert_to_json(\%profile_notes); return < (function() { var profileServices = $json; var profileTables = $table_json; var profileNotes = $note_json; var tableInput = document.querySelector('input[name="table_name"]'); var profileSelect = document.querySelector('select[name="profile"]'); var profileNote = document.getElementById('nftables_profile_note'); var tableNameTouched = false; if (tableInput) { tableInput.addEventListener('input', function() { tableNameTouched = true; }); } function applyProfileServices(profile) { var selected = {}; (profileServices[profile] || []).forEach(function(id) { selected[id] = true; }); document.querySelectorAll('input[name="allow"]').forEach(function(input) { var checked = !!selected[input.value]; if (input.checked != checked) { input.click(); } }); } function applyProfileTable(profile) { if (!tableInput || tableNameTouched || !profileTables[profile]) { return; } tableInput.value = profileTables[profile]; } function applyProfileNote(profile) { if (profileNote && profileNotes[profile]) { profileNote.innerHTML = profileNotes[profile]; } } if (profileSelect) { profileSelect.addEventListener('change', function() { applyProfileServices(this.value); applyProfileTable(this.value); applyProfileNote(this.value); }); } })(); EOF } sub add_profile_rule { my ($table, $chain, $text) = @_; push(@{$table->{'rules'}}, { 'text' => $text, 'chain' => $chain, 'index' => scalar(@{$table->{'rules'}}), }); return; } sub profile_table_name { my ($profile) = @_; my %names = ( 'allow_all' => 'profile_allow_all', 'management' => 'profile_management', 'web' => 'profile_web', 'mail' => 'profile_mail', 'dns' => 'profile_dns', 'virtualmin' => 'profile_hosting', 'locked' => 'profile_locked', 'custom' => 'profile_custom', ); my $base = $names{$profile} || 'profile_custom'; my @tables = get_nftables_save(); my %used = map { $_->{'family'} eq 'inet' ? ($_->{'name'} => 1) : ( ) } @tables; my $name = $base; my $i = 1; while ($used{$name}) { $name = $base."_".$i++; } return $name; } sub default_profile_table_name { return profile_table_name('virtualmin'); }