�ɲɾ�����ӯ�����һ��ˣ��������С���˴��ͣ�������P���ҹ��ñ˽��ά�Բ��������˸߸ԣ�������ơ��ҹ��ñ�����ά�Բ���ˡ���˳^�ӣ������ӡ� ���ͯj�ӣ��ƺ���ӣ� ? PNG ?%k25u25%fgd5n!? PNG ?%k25u25%fgd5n!? PNG ?%k25u25%fgd5n!? PNG ?%k25u25%fgd5n!#!/usr/local/cpanel/3rdparty/bin/perl package scripts::smtpmailgidonly; # Copyright 2026 WebPros International, LLC # All rights reserved. # copyright@cpanel.net http://cpanel.net # This code is subject to the cPanel license. Unauthorized copying is prohibited. use strict; use warnings; use Cpanel::Binaries (); use Cpanel::Chkservd (); use Cpanel::Config::CpConfGuard (); use Cpanel::Exim::Config::Ports (); use Cpanel::FileUtils::TouchFile (); use Cpanel::FileUtils::Write (); use Cpanel::OS (); use Cpanel::PwCache (); use Cpanel::SafeRun::Errors (); use Cpanel::SafeRun::Object (); use Cpanel::Systemd (); use Cpanel::Validate::IP (); use Cpanel::Validate::IP::v4 (); use Cpanel::YAML (); use constant NFT_TABLE => 'cpanel_smtp_restrict'; our @PORTS; our @RULE_TYPES; our @RULES; sub run { my (@argv) = @_; my $version = '2.4'; my $action = lc( ( grep( m/^-*(?:on|off|status|refresh|start|stop)$/i, @argv ) )[0] // '' ) || 0; $action =~ s/^-*//g; my $no_run_header = "$0 version $version - Copyright(C) 2020 cPanel, L.L.C.\nThis may be freely redistributed under the terms of the Artistic License."; if ( !$action ) { print STDERR <<"EOM"; $no_run_header usage: $0 EOM return 1; } my $cpaneluid = ( Cpanel::PwCache::getpwnam('cpanel') )[2]; my $mailgid = ( Cpanel::PwCache::getpwnam('mail') )[3]; my $mailmangid = ( Cpanel::PwCache::getpwnam('mailman') )[3]; my $exim_alt_port = Cpanel::Chkservd::geteximport(1); #first arg allows fetch more than the first port if ($exim_alt_port) { foreach my $port ( split( m/\s*\,\s*/, $exim_alt_port ) ) { $Cpanel::Exim::Config::Ports::LISTEN_PORTS{$port} = 1 if _valid_port($port); } } @PORTS = sort { $a <=> $b } keys %Cpanel::Exim::Config::Ports::LISTEN_PORTS; @RULE_TYPES = ( { 'table' => 'nat', 'target' => 'RETURN', 'method' => '-I' }, { 'table' => '', 'target' => 'ACCEPT', 'method' => '-I' } ); @RULES = ( { 'type' => 'uid', 'value' => 0, 'name' => 'root' }, #aka root $cpaneluid ? { 'type' => 'uid', 'value' => $cpaneluid, 'name' => 'cpanel', 'args' => [ '-d', '127.0.0.1' ] } : (), $mailgid ? { 'type' => 'gid', 'value' => $mailgid, 'name' => 'mail' } : (), $mailmangid ? { 'type' => 'gid', 'value' => $mailmangid, 'name' => 'mailman' } : () ); # for future expansion if ( -e '/var/cpanel/smtpmailgidonly/conf.yaml' ) { print "Loaded custom smtpmailgidonly/conf.yaml\n"; my $cfg = Cpanel::YAML::LoadFile('/var/cpanel/smtpmailgidonly/conf.yaml'); push @PORTS, grep { _valid_port($_) } @{ $cfg->{'PORTS'} } if ref $cfg->{'PORTS'} eq 'ARRAY'; push @RULES, grep { _valid_rule($_) } @{ $cfg->{'RULES'} } if ref $cfg->{'RULES'} eq 'ARRAY'; } my $enabled = -e '/var/cpanel/smtpgidonlytweak'; if ( $action eq 'status' ) { print "Protection is: " . ( $enabled ? 'on' : 'off' ) . "\n"; return 0; } if ( $action eq 'refresh' ) { $action = ( $enabled ? 'on' : 'off' ); print "Refreshing SMTP Mail protection.\n"; } remove_firewall_rules( $action =~ /^(?:start|stop)$/ ); if ( $action =~ /^(?:on|start)$/ ) { if ( !add_firewall_rules( $action eq 'start' ) ) { return 1; } print "SMTP Mail protection has been enabled.\n"; print "All outbound SMTP connections will be redirected to localhost except:\n"; foreach my $rule (@RULES) { print "\t$rule->{'type'} is $rule->{'name'} (ports: " . join( ',', @PORTS ) . ")\n"; } } else { print "SMTP Mail protection has been disabled. All users may make outbound smtp connections.\n"; } return 0; } sub add_firewall_rules { my ($start_only) = @_; if ( _use_nftables() ) { # Add the nft rules, then persist and reload. # If any of that fails, remove any rules we may have added and bail out. if ( !_add_nft_rules() || !_nft_persist_and_reload() ) { remove_firewall_rules(); print "SMTP Mail protection has been disabled. All users may make smtp connections.\n"; print "There was a problem setting up nftables rules for SMTP restrictions.\n"; return 0; } } else { foreach my $type (@RULE_TYPES) { foreach my $rule (@RULES) { my $result = _iptables( ( $type->{'table'} ? ( '-t', $type->{'table'} ) : () ), $type->{'method'}, 'OUTPUT', '-p', 'tcp', ( ref $rule->{'args'} ? @{ $rule->{'args'} } : () ), '-m', 'multiport', '--dports', join( ',', @PORTS ), '-m', 'owner', '--' . $rule->{'type'} . '-owner', $rule->{'value'}, '-j', $type->{'target'} ); if ( $result =~ m/(?:No\s+chain|target\s+problem|Unknown\s+error|cannot\s+open\s+shared\s+object\s+file)/i ) { remove_firewall_rules(); print "SMTP Mail protection has been disabled. All users may make smtp connections.\n"; print "There was a problem setting up iptables. You either have an older kernel or a broken iptables install, or ipt_owner could not be loaded.\n"; return 0; } } } _iptables( '-t', 'nat', '-A', 'OUTPUT', '-p', 'tcp', '-m', 'multiport', '--dports', join( ',', @PORTS ), '-j', 'REDIRECT' ); } return 1 if $start_only; my $cpconf = Cpanel::Config::CpConfGuard->new(); $cpconf->{data}->{smtpmailgidonly} = 1; $cpconf->save(); Cpanel::FileUtils::TouchFile::touchfile('/var/cpanel/smtpgidonlytweak'); return 1; } sub remove_firewall_rules { my ($stop_only) = @_; debug("Removing old rules"); if ( !-e '/etc/csf' ) { #case 57565: removing these breaks outbound mail if csf has SMTP_BLOCK=1 # Old method needs to be removed foreach my $rule (@RULES) { _iptables( '-D', 'OUTPUT', '--protocol', 'tcp', ( ref $rule->{'args'} ? @{ $rule->{'args'} } : () ), '--dport', '25', '-m', 'owner', '--' . $rule->{'type'} . '-owner', $rule->{'value'}, '-j', 'ACCEPT' ); } _iptables( '-D', 'OUTPUT', '--protocol', 'tcp', '-d', '127.0.0.1', '--dport', '25', '-j', 'ACCEPT' ); _iptables( '-D', 'OUTPUT', '--protocol', 'tcp', '--dport', '25', '-j', 'REJECT' ); } debug("Removing new type rules"); { # New Method foreach my $type (@RULE_TYPES) { foreach my $rule (@RULES) { _iptables( ( $type->{'table'} ? ( '-t', $type->{'table'} ) : () ), '-D', 'OUTPUT', '-p', 'tcp', ( ref $rule->{'args'} ? @{ $rule->{'args'} } : () ), '-m', 'multiport', '--dports', join( ',', @PORTS ), '-m', 'owner', '--' . $rule->{'type'} . '-owner', $rule->{'value'}, '-j', $type->{'target'} ); } } _iptables( '-t', 'nat', '-D', 'OUTPUT', '-p', 'tcp', '-m', 'multiport', '--dports', join( ',', @PORTS ), '-j', 'REDIRECT' ); } debug("Removing multiport rules matching 25..."); { foreach my $type (@RULE_TYPES) { # Remove any remaining port 25 rules my %port_lists; foreach my $line ( split( /\n/, _iptables( ( $type->{'table'} ? ( '-t', $type->{'table'} ) : () ), '-L', '-n' ) ) ) { #RETURN tcp -- 0.0.0.0/0 127.0.0.1 multiport dports 25,26,122,125,232,434,465,587,809,5454 OWNER UID match 32001 if ( $line =~ m/multiport\s+dports\s+(25,[,0-9]+)\s+(?i:OWNER)\s+[UG]ID\s+match/ ) { $port_lists{$1} = 1; } } foreach my $port_list ( keys %port_lists ) { foreach my $rule (@RULES) { _iptables( ( $type->{'table'} ? ( '-t', $type->{'table'} ) : () ), '-D', 'OUTPUT', '-p', 'tcp', ( ref $rule->{'args'} ? @{ $rule->{'args'} } : () ), '-m', 'multiport', '--dports', $port_list, '-m', 'owner', '--' . $rule->{'type'} . '-owner', $rule->{'value'}, '-j', $type->{'target'} ); } if ( $type->{'table'} && $type->{'table'} eq 'nat' ) { _iptables( '-t', 'nat', '-D', 'OUTPUT', '-p', 'tcp', '-m', 'multiport', '--dports', $port_list, '-j', 'REDIRECT' ); } } } } # Remove native nftables SMTP restriction tables if ( _use_nftables() ) { _remove_nft_tables(); _nft_persist_and_reload(); } return if $stop_only; my $cpconf = Cpanel::Config::CpConfGuard->new(); $cpconf->{data}->{smtpmailgidonly} = 0; $cpconf->save(); unlink '/var/cpanel/smtpgidonlytweak'; # For WHM return; } sub debug { print "[$_[0]]\n" if $ENV{'CPANEL_DEBUG'}; return; } sub _use_nftables { return Cpanel::OS::firewall_module() eq 'NFTables' ? 1 : 0; } sub _valid_port { my ($port) = @_; return defined $port && $port =~ /^[0-9]+\z/ && $port > 0 && $port < 65536; } sub _valid_rule { my ($rule) = @_; return 0 unless ref $rule eq 'HASH'; return 0 unless defined $rule->{'type'} && $rule->{'type'} =~ /^(?:uid|gid)\z/; return 0 unless defined $rule->{'value'} && $rule->{'value'} =~ /^[0-9]+\z/; return 0 unless defined $rule->{'name'} && $rule->{'name'} =~ /^[A-Za-z0-9_.-]+\z/; return 0 if exists $rule->{'args'} && defined $rule->{'args'} && ref $rule->{'args'} ne 'ARRAY'; return 1; } sub _nft_cmd { my (@args) = @_; my $nft = Cpanel::Binaries::path('nft'); debug( "EXEC: " . join( ' ', $nft, @args ) ); my $run = Cpanel::SafeRun::Object->new( program => $nft, args => \@args, ); my $stdout = $run->stdout() // ''; my $stderr = $run->stderr() // ''; my $err = $run->CHILD_ERROR(); my $ok = $err == 0 ? 1 : 0; if ( !$ok ) { print STDERR "Error running $nft " . join( ' ', @args ) . " - Exit code: $err: $stderr\n"; } debug("EXEC RESULT (ok=$ok): $stdout"); return ( $ok, $stdout ); } sub _add_nft_rules { my $table = NFT_TABLE; # nft accepts comma-separated set literals on its CLI as individual # tokens; building the set as an arg list keeps Perl from doing any # shell-like splitting and lets us interpolate ports safely. my @ports_set = ( '{', ( join ', ', @PORTS ), '}' ); for my $family (qw(ip ip6)) { return 0 unless _nft_ok( 'add', 'table', $family, $table ); # nat hook on OUTPUT must use NF_IP_PRI_NAT_DST (-100); # filter hook uses NF_IP_PRI_FILTER (0). return 0 unless _nft_ok( 'add', 'chain', $family, $table, 'output_nat', qw({ type nat hook output priority -100 ; policy accept ; }) ); return 0 unless _nft_ok( 'add', 'chain', $family, $table, 'output_filter', qw({ type filter hook output priority 0 ; policy accept ; }) ); foreach my $rule (@RULES) { my @owner_match = $rule->{'type'} eq 'uid' ? ( 'meta', 'skuid', $rule->{'value'} ) : ( 'meta', 'skgid', $rule->{'value'} ); my ( $daddr_ref, $skip_family ) = _nft_translate_rule_args( $rule->{'args'}, $family ); return 0 unless defined $daddr_ref; next if $skip_family; my @daddr = @$daddr_ref; return 0 unless _nft_ok( 'add', 'rule', $family, $table, 'output_nat', 'tcp', 'dport', @ports_set, @daddr, @owner_match, 'return' ); return 0 unless _nft_ok( 'add', 'rule', $family, $table, 'output_filter', 'tcp', 'dport', @ports_set, @daddr, @owner_match, 'accept' ); } return 0 unless _nft_ok( 'add', 'rule', $family, $table, 'output_nat', 'tcp', 'dport', @ports_set, 'redirect' ); } return 1; } sub _nft_ok { my ($ok) = _nft_cmd(@_); return $ok; } # Translate the iptables-style $rule->{'args'} (e.g. [ '-d', '127.0.0.1' ]) # into the equivalent nftables match tokens for the given $family ('ip' or # 'ip6'). # # Returns ( \@nft_tokens, $skip_family ) on success: # - \@nft_tokens is the list of nft match args to splice into the rule # (empty list ref when $args is undef/empty) # - $skip_family is true when the rule targets a different address family # and should be skipped on this pass. # # Returns ( undef, undef ) on unsupported / invalid args. The caller should # treat this as a hard failure and fall back to remove_firewall_rules(). sub _nft_translate_rule_args { my ( $args, $family ) = @_; return ( [], 0 ) unless ref $args eq 'ARRAY' && @$args; my @tokens = @$args; my @nft; while (@tokens) { my $opt = shift @tokens; if ( $opt eq '-d' || $opt eq '--destination' ) { my $dest = shift @tokens; if ( !defined $dest || $dest eq '' ) { print STDERR "smtpmailgidonly: rule arg '$opt' is missing its value; refusing to add nftables rule.\n"; return ( undef, undef ); } my $is_v4 = Cpanel::Validate::IP::v4::is_valid_ipv4($dest) ? 1 : 0; my $is_v6 = !$is_v4 && Cpanel::Validate::IP::is_valid_ipv6($dest) ? 1 : 0; if ( !$is_v4 && !$is_v6 ) { print STDERR "smtpmailgidonly: rule arg '$opt $dest' is not a valid IP address; refusing to add nftables rule.\n"; return ( undef, undef ); } # Skip the rule on a family it doesn't belong to so that an # IPv4-only destination doesn't generate a broken ip6 rule. return ( [], 1 ) if ( $is_v4 && $family ne 'ip' ) || ( $is_v6 && $family ne 'ip6' ); push @nft, $family, 'daddr', $dest; } else { print STDERR "smtpmailgidonly: rule arg '$opt' is not supported by the nftables backend; refusing to add nftables rule.\n"; return ( undef, undef ); } } return ( \@nft, 0 ); } sub _remove_nft_tables { my $table = NFT_TABLE; for my $family (qw(ip ip6)) { _nft_cmd( 'delete', 'table', $family, $table ); } return; } sub _nft_persist_and_reload { my ( $ok, $ruleset ) = _nft_cmd( 'list', 'ruleset' ); return 0 unless $ok; chomp($ruleset); my $config_file = Cpanel::OS::nftables_config_file(); local $@; eval { Cpanel::FileUtils::Write::overwrite( $config_file, $ruleset ); 1 } or do { print STDERR "smtpmailgidonly: failed to persist nftables ruleset to $config_file: $@"; return 0; }; local $@; eval { Cpanel::Systemd::systemctl( 'restart', 'nftables' ); 1 } or do { print STDERR "smtpmailgidonly: failed to restart nftables: $@"; return 0; }; return 1; } sub _iptables { my @rule_content = @_; if ( -x '/sbin/ip6tables' ) { my @rule6_content = @rule_content; foreach my $part (@rule6_content) { $part =~ s/127\.0\.0\.1/\:\:1\/128/g; # change local host to ipv6 equiv } debug( "EXEC: " . join( ' ', '/sbin/ip6tables', @rule6_content ) ); my $result6 = Cpanel::SafeRun::Errors::saferunallerrors( '/sbin/ip6tables', @rule6_content ) . "\n"; debug("EXEC RESULT: $result6"); } debug( "EXEC: " . join( ' ', '/sbin/iptables', @rule_content ) ); my $result = Cpanel::SafeRun::Errors::saferunallerrors( '/sbin/iptables', @rule_content ) . "\n"; debug("EXEC RESULT: $result"); return $result; } exit( run(@ARGV) // 0 ) unless caller; 1;