http://en.wikipedia.org/wiki/Firewall#Firewalls_in_computer_networking
The following implements a client-level firewall in slave interpreters. Slaves will only be able to access a restricted set of hosts (and ports).
################## ## Module Name -- firewall.tcl ## Original Author -- Emmanuel Frecon - [email protected] ## Description: ## ## Package to a allow a safe interpreter to access islands of the network, ## i.e. this implements a client firewall. ## ################## package require Tcl 8.4 namespace eval ::firewall { namespace eval interps {}; # Will host information for interpreters namespace export {[a-z]*}; # Convention: export all lowercase catch {namespace ensemble create} variable version 0.1 } # ::firewall::allow -- Allow host and port (patterns) # # Add a host and port pattern to the list of remote locations that are # explicitely allowed for access to a slave interpreter. # # Arguments: # slave Identifier of the slave to control # host Host/IP pattern # port Port pattern # # Results: # The current context of allowed and denied patterns # # Side Effects: # None. proc ::firewall::allow { slave { host "" } {port *} } { set vname [namespace current]::interps::[string map {: _} $slave] if { ![info exists $vname]} { Init $slave } upvar \#0 $vname context dict lappend context allow $host $port return [dict filter $context key allow deny] } # ::firewall::deny -- Deny host and port (patterns) # # Add a host and port pattern to the list of remote locations that are # explicitely denied for access to a slave interpreter. Denial is tested # after allowance, meaning that arguments to this procedure are meant to # restrict away from the allowance list. # # Arguments: # slave Identifier of the slave to control # host Host/IP pattern # port Port pattern # # Results: # The current context of allowed and denied patterns # # Side Effects: # None. proc ::firewall::deny { slave { host "*" } {port *} } { set vname [namespace current]::interps::[string map {: _} $slave] if { ![info exists $vname]} { Init $slave } upvar \#0 $vname context dict lappend context deny $host $port return [dict filter $context key allow deny] } # ::firewall::reset -- Cleanup # # Remove all access path allowance and arrange for the interpreter to be # able to return to the regular safe state. # # Arguments: # slave Identifier of the slave # # Results: # None. # # Side Effects: # None. proc ::firewall::reset { slave } { set vname [namespace current]::interps::[string map {: _} $slave] if { [info exists $vname] } { foreach cmd [list socket fconfigure encoding] { if { [dict exists $context aliases $cmd] } { $slave alias $cmd [dict get $context aliases $cmd] } else { $slave alias $cmd {} } } unset $vname } } ######################## ## ## Procedures below are internal to the implementation. ## ######################## # ::firewall::Allowed -- Check access restrictions # # Check that the file name passed as an argument is within the islands of # the filesystem that have been registered through the add command for a # given (safe) interpreter. The path is fully normalized before testing # against the islands, which themselves are fully normalized. # # Arguments: # slave Identifier of the slave under out control # fname (relative) path to the file to test # # Results: # 1 if access is allowed, 0 otherwise # # Side Effects: # None. proc ::firewall::Allowed { slave host port } { set vname [namespace current]::interps::[string map {: _} $slave] upvar \#0 $vname context set allowed 0; # Default is to deny everything! if { [dict exists $context allow] } { foreach { h p } [dict get $context allow] { if { [string match -nocase $h $host] && [string match $p $port] } { set allowed 1 break } } } if { $allowed && [dict exists $context deny] } { foreach { h p } [dict get $context deny] { if { [string match -nocase $h $host] && [string match $p $port] } { set allowed 0 break } } } return $allowed } # ::firewall::Invoke -- Expose back a command # # This procedure allows to callback a command that would typically have # been hidden from a slave interpreter. It does not "interp expose" but # rather calls the hidden command, so we can easily revert back. If # instead the command was already aliased to another command once we took # it over, call the command that it was aliased to in order to keep the # proper chain of callbacks. # # Arguments: # slave Identifier of the slave under our control # cmd Hidden command to call # args Arguments to the glob command. # # Results: # As of the hidden command to call # # Side Effects: # As of the hidden command to call proc ::firewall::Invoke { slave cmd args } { set vname [namespace current]::interps::[string map {: _} $slave] upvar \#0 $vname context if { [info exists $vname] && [dict exists $context aliases $cmd] } { # Aliased command is to be called in same interpreter as the the main # interpreter return [uplevel [dict get $context aliases $cmd] $args] } elseif { $slave eq "" } { return [uplevel [linsert $args 0 $cmd]] } else { return [uplevel [linsert $args 0 $slave invokehidden $cmd]] } } # ::firewall::Socket -- Restricted socket # # Parses the options and arguments to the glob command to discover which # paths it tries to access and only return the results of its execution # when these path are within the allowed islands of the filesystem. # # Arguments: # slave Identifier of the slave under our control # args Arguments to the glob command. # # Results: # As of the socket command. # # Side Effects: # As of the socket command. proc ::firewall::Socket { slave args } { set noargs [list -async] set opts [list] set within "" for {set i 0} {$i < [llength $args]} {incr i} { set itm [lindex $args $i] if { $itm eq "--" } { incr i; break } elseif { [string index $itm 0] eq "-" } { # Segragates between options that take a value and options that # have no arguments and are booleans. if { [lsearch $noargs $itm] < 0 } { incr i; # Jump over argument switch -glob -- $itm { "-myp*" - "-mya*" { # Capture -myhost and -myport lappend opts $itm [lindex $args $i] } "-s*" { return -code error "Server socket not allowed!" } } } else { lappend opts $itm } } else { break } } # cut off remaining arguments and check for allowance of host and port # specified there. set args [lrange $args $i end] if { [llength $args] < 2 } { return -code error "Missing host or port specification!" } foreach {host port} $args break if { ![Allowed $slave $host $port] } { return -code error "Access to ${host}:${port} prevented by firewall" } # Reconstruct call and pass further lappend opts $host $port; # Reinsert host and port at end of options return [uplevel [linsert $args 0 [namespace current]::Invoke $slave socket]] } # ::firewall::Alias -- Careful aliasing # # Create an alias to an existing into this library, making sure to # remember where the command was already aliased to whenever relevant. # # Arguments: # slave Identifier of the slave to control # cmd Command to alias # args Additional arguments to command # # Results: # None. # # Side Effects: # None. proc ::firewall::Alias { slave cmd args } { set vname [namespace current]::interps::[string map {: _} $slave] upvar \#0 $vname context if { ![info exists $vname] || ![dict exists $context aliases $cmd] } { set alias [$slave alias $cmd] if { $alias ne "" } { dict set context aliases $cmd $alias } } return [uplevel [linsert $args 0 $slave alias $cmd]] } # ::firewall::Init -- Initialise interp # # Initialise slave interpreter so that it will be able to perform some # file operations, but only within some islands of the filesystem. # # Arguments: # slave Identifier of the slave to control # # Results: # None. # # Side Effects: # None. proc ::firewall::Init { slave } { Alias $slave socket ::firewall::Socket $slave Alias $slave fconfigure ::firewall::Invoke $slave fconfigure Alias $slave encoding ::firewall::Invoke $slave encoding } package provide firewall $::firewall::version
This code can be exercised as follows:
package require firewall # Create an interp and allow it to access wiki.tcl.tk. set i [interp create -safe] ::firewall::allow $i wiki.tcl.tk 80 # From https://wiki.tcl-lang.org/17394 proc get_package_load_command {name} { # Get the command to load a package without actually loading the package # # package ifneeded can return us the command to load a package but # it needs a version number. package versions will give us that set versions [package versions $name] if {[llength $versions] == 0} { # We do not know about this package yet. Invoke package unknown # to search {*}[package unknown] $name # Check again if we found anything set versions [package versions $name] if {[llength $versions] == 0} { error "Could not find package $name" } } return [package ifneeded $name [lindex $versions 0]] } # Read content of file implementing http library. This will only work for # modules, really... set fname [lindex [get_package_load_command http] end] if { [file exists $fname] } { set fd [open $fname] set http [read $fd] close $fd } else { puts stderr "Cannot find any implementation for http" } # Pass content of tcl_platform array to HTTP implementation, it needs it for # creating the agent string. $i eval [list array set ::tcl_platform [array get tcl_platform]] # Pass content of HTTP implementation, this supposes a modern Tcl where HTTP is # implemented as a single module. $i eval $http interp share {} stdout $i; # Give away stdout so we can output data $i eval { set t [::http::geturl https://wiki.tcl-lang.org/] if { [::http::data $t] ne "" } { puts "Properly downloaded [string length [::http::data $t]] char(s) from wiki.tck.tk" } # Make it fail on another host. set t [::http::geturl https://www.tcl-lang.org/] }